abserde/
lib.rs

1//! Simple platform-agnostic Rust crate for managing application settings/preferences.
2//!
3//! # Installation
4//!
5//! Install the crate as a dependency in your app's Cargo.toml file:
6//!
7//! ```toml
8//! [dependencies]
9//! abserde = "0.6.0"
10//! ```
11//!
12//! # Usage
13//! Import [Abserde], associated definitions, and [serde::Serialize], and [serde::Deserialize]:
14//!
15//! ```no_run
16//! use abserde::*;
17//! use serde::{Serialize, Deserialize};
18//! ```
19//!
20//! Define a struct to store your app config.
21//! You must derive your struct from [serde::Serialize] and [serde::Deserialize] traits.
22//!
23//! ```no_run
24//! use std::collections::HashMap;
25//!
26//! # use serde::{Serialize, Deserialize};
27//! #
28//! #[derive(Serialize, Deserialize)]
29//! struct MyConfig {
30//! 	window_width: usize,
31//! 	window_height: usize,
32//! 	window_x: usize,
33//! 	window_y: usize,
34//! 	theme: String,
35//! 	user_data: HashMap<String, String>,
36//! }
37//! ```
38//!
39//! Create an [Abserde] instance to manage how your configuration is stored on disk:
40//!
41//! ```no_run
42//! # use serde::{Serialize, Deserialize};
43//! #
44//! # use abserde::*;
45//! #
46//! # #[derive(Serialize, Deserialize)]
47//! # struct MyConfig;
48//! #
49//! let my_abserde = Abserde::default();
50//! ```
51//!
52//! Using [Abserde] in this way will use your crate as the name for the app config directory.
53//!
54//! Alternatively, you can also pass options to [Abserde] to change the location or format of your config file:
55//!
56//! ```no_run
57//! # use serde::{Serialize, Deserialize};
58//! #
59//! # use abserde::*;
60//! #
61//! # #[derive(Serialize, Deserialize)]
62//! # struct MyConfig;
63//! #
64//! let my_abserde = Abserde {
65//! 	app: "MyApp".to_string(),
66//! 	location: Location::Auto,
67//! 	format: Format::Json,
68//! };
69//! ```
70//!
71//! For the JSON format, you can pretty-print your config file, using either tabs or spaces:
72//!
73//! ```no_run
74//! # use serde::{Serialize, Deserialize};
75//! #
76//! # use abserde::*;
77//! #
78//! # #[derive(Serialize, Deserialize)]
79//! # struct MyConfig;
80//! #
81//! let my_abserde = Abserde {
82//! 	app: "MyApp".to_string(),
83//! 	location: Location::Auto,
84//! 	format: Format::PrettyJson(PrettyJsonIndent::Tab),
85//! };
86//! ```
87//!
88//! ```no_run
89//! # use serde::{Serialize, Deserialize};
90//! #
91//! # use abserde::*;
92//! #
93//! # #[derive(Serialize, Deserialize)]
94//! # struct MyConfig;
95//! #
96//! let my_abserde = Abserde {
97//! 	app: "MyApp".to_string(),
98//! 	location: Location::Auto,
99//! 	format: Format::PrettyJson(PrettyJsonIndent::Spaces(4)),
100//! };
101//! ```
102//!
103//! Load data into config from disk:
104//!
105//! ```no_run
106//! # use serde::{Serialize, Deserialize};
107//! #
108//! # use abserde::*;
109//! #
110//! # #[derive(Serialize, Deserialize)]
111//! # struct MyConfig;
112//! #
113//! # let my_abserde = Abserde {
114//! # 	app: "MyApp".to_string(),
115//! # 	location: Location::Auto,
116//! # 	format: Format::Json,
117//! # };
118//! #
119//! let my_config = MyConfig::load_config(&my_abserde)?;
120//! #
121//! # Ok::<(), Error>(())
122//! ```
123//!
124//! Save config data to disk:
125//!
126//! ```no_run
127//! # use serde::{Serialize, Deserialize};
128//! #
129//! # use abserde::*;
130//! #
131//! # #[derive(Serialize, Deserialize)]
132//! # struct MyConfig;
133//! #
134//! # let my_abserde = Abserde {
135//! # 	app: "MyApp".to_string(),
136//! # 	location: Location::Auto,
137//! # 	format: Format::Json,
138//! # };
139//! #
140//! # let my_config = MyConfig::load_config(&my_abserde)?;
141//! my_config.save_config(&my_abserde)?;
142//! #
143//! # Ok::<(), Error>(())
144//! ```
145//!
146//! Delete config file from disk:
147//!
148//! ```no_run
149//! # use serde::{Serialize, Deserialize};
150//! #
151//! # use abserde::*;
152//! #
153//! # #[derive(Serialize, Deserialize)]
154//! # struct MyConfig;
155//! #
156//! # let my_abserde = Abserde {
157//! # 	app: "MyApp".to_string(),
158//! # 	location: Location::Auto,
159//! # 	format: Format::Json,
160//! # };
161//! #
162//! # let my_config = MyConfig::load_config(&my_abserde)?;
163//! # my_config.save_config(&my_abserde)?;
164//! #
165//! my_abserde.delete()?;
166//!
167//! # Ok::<(), Error>(())
168//! ```
169
170#![deny(missing_docs)]
171#![allow(clippy::tabs_in_doc_comments)]
172
173use std::env::var;
174use std::fmt::Display;
175use std::fs::{create_dir_all, remove_dir, remove_file, File};
176use std::path::PathBuf;
177use std::str;
178use std::{io, result};
179
180use serde::{de::DeserializeOwned, Serialize};
181
182const MSG_NO_SYSTEM_CONFIG_DIR: &str = "no system config directory detected";
183
184/// Alias for generic Error type.
185pub type Error = anyhow::Error;
186
187/// Alias for Result type wrapping generic Error type.
188pub type Result<T> = result::Result<T, Error>;
189
190/// JSON pretty print indentation style selection.
191#[derive(Debug, PartialEq, Clone, Default)]
192pub enum PrettyJsonIndent {
193	/// Indent using a tab character.
194	#[default]
195	Tab,
196
197	/// Indent using a given number of space characters.
198	Spaces(usize),
199}
200
201impl Display for PrettyJsonIndent {
202	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203		match self {
204			PrettyJsonIndent::Tab => write!(f, "\t"),
205			PrettyJsonIndent::Spaces(n) => write!(f, "{}", " ".repeat(*n)),
206		}
207	}
208}
209
210/// Storage format for app config.
211///
212/// Each format is enabled as a feature. The json feature is included by default.
213/// All other format features are disabled by default.
214#[derive(Debug, PartialEq, Clone, Default)]
215pub enum Format {
216	// Default will become the first supported in order of preference.
217	#[default]
218
219	/// JSON format using the serde_json crate.
220	#[cfg(feature = "json")]
221	Json,
222
223	/// JSON pretty-printed format using serde_json crate.
224	#[cfg(feature = "json")]
225	PrettyJson(PrettyJsonIndent),
226
227	/// YAML format using the serde_yaml crate.
228	#[cfg(feature = "yaml")]
229	Yaml,
230
231	/// Pickle (Python) format using the serde-pickle crate.
232	#[cfg(feature = "pickle")]
233	Pickle,
234
235	/// INI (Windows) format using the serde_ini crate.
236	#[cfg(feature = "ini")]
237	Ini,
238
239	/// TOML format using the toml crate.
240	#[cfg(feature = "toml")]
241	Toml,
242}
243
244impl Format {
245	/// Return default file name of config file for this format.
246	pub fn default_name(&self) -> String {
247		match self {
248			Format::PrettyJson(_) => format!("config.{:?}", Format::Json).to_lowercase(),
249			_ => format!("config.{:?}", self).to_lowercase(),
250		}
251	}
252}
253
254/// Represents the location of a config file.
255#[derive(Debug, PartialEq, Clone, Default)]
256pub enum Location {
257	/// Automatically determines location of config file based on platform/OS.
258	#[default]
259	Auto,
260
261	/// Provides the full path to the config file.
262	Path(PathBuf),
263
264	/// Automatically determines config directory, with file name specified manually.
265	File(PathBuf),
266
267	/// Automatically determines config file name, with directory specified manually.
268	Dir(PathBuf),
269}
270
271/// Represents an Abserde app, specifying how app settings are to be managed.
272#[derive(Debug, PartialEq, Clone)]
273pub struct Abserde {
274	/// App name under which app settings are typically to be stored.
275	pub app: String,
276
277	/// Location specification for where app settings are physically kept.
278	pub location: Location,
279
280	/// Format for app setting storage and serialisation.
281	pub format: Format,
282}
283
284impl Abserde {
285	fn config_path(&self) -> Result<PathBuf> {
286		let system_config_dir = dirs::config_dir()
287			.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, MSG_NO_SYSTEM_CONFIG_DIR))?;
288
289		Ok(match &self.location {
290			Location::Auto => system_config_dir
291				.join(&self.app)
292				.join(&self.format.default_name()),
293			Location::Path(path) => path.clone(),
294			Location::Dir(dir) => dir.join(&self.format.default_name()),
295			Location::File(file) => system_config_dir.join(&self.app).join(file),
296		})
297	}
298
299	/// Delete settings file related to this app.
300	pub fn delete(&self) -> Result<()> {
301		let config_path = self.config_path()?;
302
303		remove_file(&config_path)?;
304
305		match &self.location {
306			// Don't attempt to delete folder if manually specifying folder.
307			Location::Dir(_) => {}
308			// Attempt to delete parent folder if it is empty.
309			_ => {
310				let config_dir = config_path.parent().ok_or_else(|| {
311					io::Error::new(io::ErrorKind::NotFound, MSG_NO_SYSTEM_CONFIG_DIR)
312				})?;
313
314				// Ignore any errors here, as they are sometimes expected.
315				_ = remove_dir(config_dir);
316			}
317		}
318
319		Ok(())
320	}
321}
322
323impl Default for Abserde {
324	fn default() -> Self {
325		Self {
326			app: var("CARGO_PKG_NAME").unwrap_or_else(|_| env!("CARGO_PKG_NAME").to_string()),
327			location: Default::default(),
328			format: Default::default(),
329		}
330	}
331}
332
333/// Trait that apps can implement to store app settings.
334///
335/// Implementing types must also implement [serde::Serialize] and [serde::Deserialize] traits.
336pub trait Config {
337	/// Type of implementation.
338	type T;
339
340	/// Load a config from disk into the implementing type.
341	fn load_config(abserde: &Abserde) -> Result<Self::T>;
342
343	/// Save a config from the implementing type to disk.
344	fn save_config(&self, abserde: &Abserde) -> Result<()>;
345}
346
347impl<T> Config for T
348where
349	T: Serialize,
350	T: DeserializeOwned,
351{
352	type T = T;
353
354	fn load_config(abserde: &Abserde) -> Result<Self::T> {
355		let config_path = abserde.config_path()?;
356
357		Ok(match abserde.format {
358			#[cfg(feature = "json")]
359			Format::Json | Format::PrettyJson(_) => {
360				let file = File::open(config_path)?;
361
362				serde_json::from_reader(io::BufReader::new(file))?
363			}
364			#[cfg(feature = "yaml")]
365			Format::Yaml => {
366				let file = File::open(config_path)?;
367
368				serde_yaml::from_reader(io::BufReader::new(file))?
369			}
370			#[cfg(feature = "pickle")]
371			Format::Pickle => {
372				let file = File::open(config_path)?;
373
374				serde_pickle::from_reader(io::BufReader::new(file), serde_pickle::DeOptions::new())?
375			}
376			#[cfg(feature = "ini")]
377			Format::Ini => {
378				let file = File::open(config_path)?;
379
380				serde_ini::from_read(io::BufReader::new(file))?
381			}
382			#[cfg(feature = "toml")]
383			Format::Toml => {
384				use io::Read;
385
386				let mut file = File::open(config_path)?;
387				let mut buf = String::new();
388
389				file.read_to_string(&mut buf)?;
390
391				toml::from_str(&buf)?
392			}
393		})
394	}
395
396	fn save_config(&self, abserde: &Abserde) -> Result<()> {
397		let config_path = abserde.config_path()?;
398		let config_dir = config_path
399			.parent()
400			.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, MSG_NO_SYSTEM_CONFIG_DIR))?;
401
402		create_dir_all(config_dir)?;
403
404		match &abserde.format {
405			#[cfg(feature = "json")]
406			Format::Json => {
407				serde_json::to_writer(File::create(&config_path)?, self)?;
408			}
409			#[cfg(feature = "json")]
410			Format::PrettyJson(indent) => {
411				use io::Write;
412
413				let mut buf = Vec::new();
414				let indent_string = indent.to_string();
415				let formatter =
416					serde_json::ser::PrettyFormatter::with_indent(indent_string.as_bytes());
417				let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
418				self.serialize(&mut ser).unwrap();
419
420				write!(File::create(&config_path)?, "{}\n", String::from_utf8(buf)?)?;
421			}
422			#[cfg(feature = "yaml")]
423			Format::Yaml => {
424				serde_yaml::to_writer(File::create(&config_path)?, self)?;
425			}
426			#[cfg(feature = "pickle")]
427			Format::Pickle => {
428				serde_pickle::to_writer(
429					&mut File::create(&config_path)?,
430					self,
431					serde_pickle::SerOptions::new(),
432				)?;
433			}
434			#[cfg(feature = "ini")]
435			Format::Ini => {
436				serde_ini::to_writer(File::create(&config_path)?, self)?;
437			}
438			#[cfg(feature = "toml")]
439			Format::Toml => {
440				use io::Write;
441
442				write!(File::create(&config_path)?, "{}", toml::to_string(self)?)?;
443			}
444		}
445
446		Ok(())
447	}
448}
449
450#[cfg(test)]
451mod tests {
452	use std::collections::HashMap;
453	use std::fmt::Debug;
454
455	use fake::{Dummy, Fake, Faker};
456	use serde::{de::DeserializeOwned, Deserialize, Serialize};
457	use serial_test::serial;
458	use tempfile::{NamedTempFile, TempDir};
459
460	use crate::{Abserde, Config, Format, Location, PrettyJsonIndent};
461
462	const APP_NAME: &str = env!("CARGO_PKG_NAME");
463
464	// Test config type for serialisation formats that only accept basic types.
465	#[derive(Serialize, Deserialize, Debug, Default, Dummy, PartialEq)]
466	struct TestConfigSimple {
467		string_val: String,
468		i8_val: i8,
469		i16_val: i16,
470		i32_val: i32,
471		u8_val: u8,
472		u16_val: u16,
473		u32_val: u32,
474		f32_val: f32,
475	}
476
477	// More complex config type for serialisation formats that support advanced types.
478	#[derive(Serialize, Deserialize, Debug, Default, Dummy, PartialEq)]
479	struct TestConfigComplex {
480		string_val: String,
481		i8_val: i8,
482		i16_val: i16,
483		i32_val: i32,
484		i64_val: i64,
485		i128_val: i128,
486		u8_val: u8,
487		u16_val: u16,
488		u32_val: u32,
489		u64_val: u64,
490		u128_val: u128,
491		f32_val: f32,
492		f64_val: f64,
493		vec_1_val: Vec<i64>,
494		vec_2_val: Vec<(String, i8, i8, i32, i64, String, String, String)>,
495		vec_3_val: Vec<(f32, f32, f32, f64, f64)>,
496		hash_map_1_val: HashMap<String, String>,
497		hash_map_2_val: HashMap<i16, i64>,
498		hash_map_3_val: HashMap<i16, Vec<(String, i32, u8, HashMap<String, i32>)>>,
499		hash_map_4_val: HashMap<String, (f64, f32, i8)>,
500	}
501
502	// Generic dispatch method.
503	fn test_save_load_delete<T>(abserde: &Abserde)
504	where
505		T: Serialize,
506		T: DeserializeOwned,
507		T: Dummy<Faker>,
508		T: PartialEq,
509		T: Debug,
510	{
511		let test_config_saved: T = Faker.fake();
512
513		test_config_saved.save_config(&abserde).unwrap();
514
515		let test_config_loaded = T::load_config(&abserde).unwrap();
516
517		assert_eq!(test_config_saved, test_config_loaded);
518
519		abserde.delete().unwrap();
520	}
521
522	#[test]
523	#[serial]
524	fn test_auto() {
525		test_save_load_delete::<TestConfigSimple>(&Abserde::default());
526	}
527
528	#[cfg(feature = "json")]
529	#[test]
530	#[serial]
531	fn test_json_auto() {
532		test_save_load_delete::<TestConfigComplex>(&Abserde {
533			app: APP_NAME.to_string(),
534			location: Location::Auto,
535			format: Format::Json,
536		});
537	}
538
539	#[cfg(feature = "json")]
540	#[test]
541	#[serial]
542	fn test_pretty_json_auto() {
543		test_save_load_delete::<TestConfigComplex>(&Abserde {
544			app: APP_NAME.to_string(),
545			location: Location::Auto,
546			format: Format::PrettyJson(PrettyJsonIndent::default()),
547		});
548	}
549
550	#[cfg(feature = "json")]
551	#[test]
552	fn test_json_path() {
553		let tmp_file = NamedTempFile::new().unwrap();
554
555		test_save_load_delete::<TestConfigComplex>(&Abserde {
556			app: APP_NAME.to_string(),
557			location: Location::Path(tmp_file.path().into()),
558			format: Format::Json,
559		});
560	}
561
562	#[cfg(feature = "json")]
563	#[test]
564	fn test_pretty_json_path() {
565		let tmp_file = NamedTempFile::new().unwrap();
566
567		test_save_load_delete::<TestConfigComplex>(&Abserde {
568			app: APP_NAME.to_string(),
569			location: Location::Path(tmp_file.path().into()),
570			format: Format::PrettyJson(PrettyJsonIndent::Spaces(4)),
571		});
572	}
573
574	#[cfg(feature = "json")]
575	#[test]
576	#[serial]
577	fn test_json_file() {
578		test_save_load_delete::<TestConfigComplex>(&Abserde {
579			app: APP_NAME.to_string(),
580			location: Location::File("custom_file.json".into()),
581			format: Format::Json,
582		});
583	}
584
585	#[cfg(feature = "json")]
586	#[test]
587	#[serial]
588	fn test_pretty_json_file() {
589		test_save_load_delete::<TestConfigComplex>(&Abserde {
590			app: APP_NAME.to_string(),
591			location: Location::File("custom_file.json".into()),
592			format: Format::PrettyJson(PrettyJsonIndent::Spaces(4)),
593		});
594	}
595
596	#[cfg(feature = "json")]
597	#[test]
598	fn test_json_dir() {
599		let tmp_dir = TempDir::new().unwrap();
600
601		test_save_load_delete::<TestConfigComplex>(&Abserde {
602			app: APP_NAME.to_string(),
603			location: Location::Dir(tmp_dir.path().into()),
604			format: Format::Json,
605		});
606	}
607
608	#[cfg(feature = "json")]
609	#[test]
610	fn test_pretty_json_dir() {
611		let tmp_dir = TempDir::new().unwrap();
612
613		test_save_load_delete::<TestConfigComplex>(&Abserde {
614			app: APP_NAME.to_string(),
615			location: Location::Dir(tmp_dir.path().into()),
616			format: Format::PrettyJson(PrettyJsonIndent::Spaces(4)),
617		});
618	}
619
620	#[cfg(feature = "yaml")]
621	#[test]
622	#[serial]
623	fn test_yaml_auto() {
624		test_save_load_delete::<TestConfigComplex>(&Abserde {
625			app: APP_NAME.to_string(),
626			location: Location::Auto,
627			format: Format::Yaml,
628		});
629	}
630
631	#[cfg(feature = "yaml")]
632	#[test]
633	fn test_yaml_path() {
634		let tmp_file = NamedTempFile::new().unwrap();
635
636		test_save_load_delete::<TestConfigComplex>(&Abserde {
637			app: APP_NAME.to_string(),
638			location: Location::Path(tmp_file.path().into()),
639			format: Format::Yaml,
640		});
641	}
642
643	#[cfg(feature = "yaml")]
644	#[test]
645	#[serial]
646	fn test_yaml_file() {
647		test_save_load_delete::<TestConfigComplex>(&Abserde {
648			app: APP_NAME.to_string(),
649			location: Location::File("custom_file.yaml".into()),
650			format: Format::Yaml,
651		});
652	}
653
654	#[cfg(feature = "yaml")]
655	#[test]
656	fn test_yaml_dir() {
657		let tmp_dir = TempDir::new().unwrap();
658
659		test_save_load_delete::<TestConfigComplex>(&Abserde {
660			app: APP_NAME.to_string(),
661			location: Location::Dir(tmp_dir.path().into()),
662			format: Format::Yaml,
663		});
664	}
665
666	#[cfg(feature = "pickle")]
667	#[test]
668	#[serial]
669	fn test_pickle_auto() {
670		test_save_load_delete::<TestConfigSimple>(&Abserde {
671			app: APP_NAME.to_string(),
672			location: Location::Auto,
673			format: Format::Pickle,
674		});
675	}
676
677	#[cfg(feature = "pickle")]
678	#[test]
679	fn test_pickle_path() {
680		let tmp_file = NamedTempFile::new().unwrap();
681
682		test_save_load_delete::<TestConfigSimple>(&Abserde {
683			app: APP_NAME.to_string(),
684			location: Location::Path(tmp_file.path().into()),
685			format: Format::Pickle,
686		});
687	}
688
689	#[cfg(feature = "pickle")]
690	#[test]
691	#[serial]
692	fn test_pickle_file() {
693		test_save_load_delete::<TestConfigSimple>(&Abserde {
694			app: APP_NAME.to_string(),
695			location: Location::File("custom_file.pickle".into()),
696			format: Format::Pickle,
697		});
698	}
699
700	#[cfg(feature = "pickle")]
701	#[test]
702	fn test_pickle_dir() {
703		let tmp_dir = TempDir::new().unwrap();
704
705		test_save_load_delete::<TestConfigSimple>(&Abserde {
706			app: APP_NAME.to_string(),
707			location: Location::Dir(tmp_dir.path().into()),
708			format: Format::Pickle,
709		});
710	}
711
712	#[cfg(feature = "ini")]
713	#[test]
714	#[serial]
715	fn test_ini_auto() {
716		test_save_load_delete::<TestConfigSimple>(&Abserde {
717			app: APP_NAME.to_string(),
718			location: Location::Auto,
719			format: Format::Ini,
720		});
721	}
722
723	#[cfg(feature = "ini")]
724	#[test]
725	fn test_ini_path() {
726		let tmp_file = NamedTempFile::new().unwrap();
727
728		test_save_load_delete::<TestConfigSimple>(&Abserde {
729			app: APP_NAME.to_string(),
730			location: Location::Path(tmp_file.path().into()),
731			format: Format::Ini,
732		});
733	}
734
735	#[cfg(feature = "ini")]
736	#[test]
737	#[serial]
738	fn test_ini_file() {
739		test_save_load_delete::<TestConfigSimple>(&Abserde {
740			app: APP_NAME.to_string(),
741			location: Location::File("custom_file.ini".into()),
742			format: Format::Ini,
743		});
744	}
745
746	#[cfg(feature = "ini")]
747	#[test]
748	fn test_ini_dir() {
749		let tmp_dir = TempDir::new().unwrap();
750
751		test_save_load_delete::<TestConfigSimple>(&Abserde {
752			app: APP_NAME.to_string(),
753			location: Location::Dir(tmp_dir.path().into()),
754			format: Format::Ini,
755		});
756	}
757
758	#[cfg(feature = "toml")]
759	#[test]
760	#[serial]
761	fn test_toml_auto() {
762		test_save_load_delete::<TestConfigSimple>(&Abserde {
763			app: APP_NAME.to_string(),
764			location: Location::Auto,
765			format: Format::Toml,
766		});
767	}
768
769	#[cfg(feature = "toml")]
770	#[test]
771	fn test_toml_path() {
772		let tmp_file = NamedTempFile::new().unwrap();
773
774		test_save_load_delete::<TestConfigSimple>(&Abserde {
775			app: APP_NAME.to_string(),
776			location: Location::Path(tmp_file.path().into()),
777			format: Format::Toml,
778		});
779	}
780
781	#[cfg(feature = "toml")]
782	#[test]
783	#[serial]
784	fn test_toml_file() {
785		test_save_load_delete::<TestConfigSimple>(&Abserde {
786			app: APP_NAME.to_string(),
787			location: Location::File("custom_file.toml".into()),
788			format: Format::Toml,
789		});
790	}
791
792	#[cfg(feature = "toml")]
793	#[test]
794	fn test_toml_dir() {
795		let tmp_dir = TempDir::new().unwrap();
796
797		test_save_load_delete::<TestConfigSimple>(&Abserde {
798			app: APP_NAME.to_string(),
799			location: Location::Dir(tmp_dir.path().into()),
800			format: Format::Toml,
801		});
802	}
803}