From 45bfa320a59894912690a0696ef5006a15ef4650 Mon Sep 17 00:00:00 2001 From: Ricky Hosfelt Date: Sun, 25 May 2025 14:44:55 -0400 Subject: [PATCH 1/4] Remove directories crate for dirs and introduce xdg feature to allow configs to be stored in xdg defaults --- Cargo.toml | 3 +- examples/simple.rs | 5 ++-- src/lib.rs | 68 ++++++++++++++++++++++++++++++++++------------ 3 files changed, 54 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9584f4b..744e622 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ edition = "2024" [dependencies] ron = { version = "0.10.1", optional = true } -directories = "6" +etcetera = "0.10.0" serde = "^1.0" serde_yaml = { version = "0.9", optional = true } thiserror = "2.0" @@ -24,6 +24,7 @@ toml_conf = ["toml"] basic_toml_conf = ["basic-toml"] yaml_conf = ["serde_yaml"] ron_conf = ["ron"] +xdg = [] [[example]] name = "simple" diff --git a/examples/simple.rs b/examples/simple.rs index 1e8f9cf..8cae819 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -41,7 +41,7 @@ fn main() -> Result<(), confy::ConfyError> { name: "Test".to_string(), ..cfg }; - confy::store("confy_simple_app",None, &cfg)?; + confy::store("confy_simple_app", None, &cfg)?; println!("The updated toml file content is:"); let mut content = String::new(); std::fs::File::open(&file) @@ -53,7 +53,6 @@ fn main() -> Result<(), confy::ConfyError> { name: "Test".to_string(), ..cfg }; - std::fs::remove_dir_all(file.parent().unwrap()) - .expect("Failed to remove directory"); + std::fs::remove_dir_all(file.parent().unwrap()).expect("Failed to remove directory"); Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index a34d4f0..c465111 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,9 +73,9 @@ //! //! ### Tip //! to add this crate to your project with the default, toml config do the following: `cargo add confy`, otherwise do something like: `cargo add confy --no-default-features --features yaml_conf`, for more info, see [cargo docs on features] -//! +//! //! [cargo docs on features]: https://docs.rust-lang.org/cargo/reference/resolver.html#features -//! +//! //! feature | file format | description //! ------- | ----------- | ----------- //! **default**: `toml_conf` | [toml] | considered a reasonable default, uses the standard-compliant [`toml` crate] @@ -94,8 +94,12 @@ mod utils; use utils::*; -use directories::ProjectDirs; -use serde::{de::DeserializeOwned, Serialize}; +#[cfg(feature = "xdg")] +use etcetera::app_strategy::choose_app_strategy; +#[cfg(not(feature = "xdg"))] +use etcetera::app_strategy::choose_native_strategy; +use etcetera::{AppStrategy, AppStrategyArgs}; +use serde::{Serialize, de::DeserializeOwned}; use std::fs::{self, File, OpenOptions, Permissions}; use std::io::{ErrorKind::NotFound, Write}; use std::path::{Path, PathBuf}; @@ -109,8 +113,8 @@ use toml::{ #[cfg(feature = "basic_toml_conf")] use basic_toml::{ - from_str as toml_from_str, to_string as toml_to_string_pretty, Error as TomlDeErr, - Error as TomlSerErr, + Error as TomlDeErr, Error as TomlSerErr, from_str as toml_from_str, + to_string as toml_to_string_pretty, }; #[cfg(not(any( @@ -469,23 +473,49 @@ pub fn get_configuration_file_path<'a>( config_name: impl Into>, ) -> Result { let config_name = config_name.into().unwrap_or("default-config"); - let project = ProjectDirs::from("rs", "", app_name).ok_or_else(|| { - ConfyError::BadConfigDirectory("could not determine home directory path".to_string()) - })?; + let project; + + #[cfg(not(feature = "xdg"))] + { + project = choose_native_strategy(AppStrategyArgs { + top_level_domain: "rs".to_string(), + author: "".to_string(), + app_name: app_name.to_string(), + }) + .map_err(|e| { + ConfyError::BadConfigDirectory(format!("could not determine home directory path: {e}")) + })?; + } + + #[cfg(feature = "xdg")] + { + project = choose_app_strategy(AppStrategyArgs { + top_level_domain: "rs".to_string(), + author: "".to_string(), + app_name: app_name.to_string(), + }) + .map_err(|e| { + ConfyError::BadConfigDirectory(format!("could not determine home directory path: {e}")) + })?; + } let config_dir_str = get_configuration_directory_str(&project)?; - let path = [config_dir_str, &format!("{config_name}.{EXTENSION}")] + let path = [config_dir_str, format!("{config_name}.{EXTENSION}")] .iter() .collect(); Ok(path) } -fn get_configuration_directory_str(project: &ProjectDirs) -> Result<&str, ConfyError> { - let path = project.config_dir(); - path.to_str() - .ok_or_else(|| ConfyError::BadConfigDirectory(format!("{path:?} is not valid Unicode"))) +fn get_configuration_directory_str(project: &impl AppStrategy) -> Result { + let path = if cfg!(feature = "xdg") { + project.config_dir() + } else { + project.data_dir() + }; + + Ok(format!("{}", path.display())) } #[cfg(test)] @@ -601,10 +631,12 @@ mod tests { store_path_perms(path, &config, permissions).expect("store_path_perms failed"); - assert!(fs::metadata(path) - .expect("reading metadata failed") - .permissions() - .readonly()); + assert!( + fs::metadata(path) + .expect("reading metadata failed") + .permissions() + .readonly() + ); }) } From 35440730d699b2da49ee8db3c38a7cd489ed8d88 Mon Sep 17 00:00:00 2001 From: Ricky Hosfelt Date: Tue, 22 Jul 2025 20:58:12 -0400 Subject: [PATCH 2/4] Remove feature gates and full move to XDG and etcetera --- Cargo.toml | 5 ++--- src/lib.rs | 47 ++++++++++------------------------------------- 2 files changed, 12 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 744e622..0868168 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "confy" -version = "1.0.0" +version = "2.0.0" authors = ["Katharina Fey "] description = "Boilerplate-free configuration management" license = "MIT/X11 OR Apache-2.0" @@ -16,7 +16,7 @@ serde = "^1.0" serde_yaml = { version = "0.9", optional = true } thiserror = "2.0" basic-toml = { version = "0.1.10", optional = true } -toml = { version = "0.8", optional = true } +toml = { version = "0.9", optional = true } [features] default = ["toml_conf"] @@ -24,7 +24,6 @@ toml_conf = ["toml"] basic_toml_conf = ["basic-toml"] yaml_conf = ["serde_yaml"] ron_conf = ["ron"] -xdg = [] [[example]] name = "simple" diff --git a/src/lib.rs b/src/lib.rs index c465111..5de8abd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,11 +94,7 @@ mod utils; use utils::*; -#[cfg(feature = "xdg")] -use etcetera::app_strategy::choose_app_strategy; -#[cfg(not(feature = "xdg"))] -use etcetera::app_strategy::choose_native_strategy; -use etcetera::{AppStrategy, AppStrategyArgs}; +use etcetera::{AppStrategy, AppStrategyArgs, app_strategy::choose_app_strategy}; use serde::{Serialize, de::DeserializeOwned}; use std::fs::{self, File, OpenOptions, Permissions}; use std::io::{ErrorKind::NotFound, Write}; @@ -473,31 +469,14 @@ pub fn get_configuration_file_path<'a>( config_name: impl Into>, ) -> Result { let config_name = config_name.into().unwrap_or("default-config"); - let project; - - #[cfg(not(feature = "xdg"))] - { - project = choose_native_strategy(AppStrategyArgs { - top_level_domain: "rs".to_string(), - author: "".to_string(), - app_name: app_name.to_string(), - }) - .map_err(|e| { - ConfyError::BadConfigDirectory(format!("could not determine home directory path: {e}")) - })?; - } - - #[cfg(feature = "xdg")] - { - project = choose_app_strategy(AppStrategyArgs { - top_level_domain: "rs".to_string(), - author: "".to_string(), - app_name: app_name.to_string(), - }) - .map_err(|e| { - ConfyError::BadConfigDirectory(format!("could not determine home directory path: {e}")) - })?; - } + let project = choose_app_strategy(AppStrategyArgs { + top_level_domain: "rs".to_string(), + author: "".to_string(), + app_name: app_name.to_string(), + }) + .map_err(|e| { + ConfyError::BadConfigDirectory(format!("could not determine home directory path: {e}")) + })?; let config_dir_str = get_configuration_directory_str(&project)?; @@ -509,13 +488,7 @@ pub fn get_configuration_file_path<'a>( } fn get_configuration_directory_str(project: &impl AppStrategy) -> Result { - let path = if cfg!(feature = "xdg") { - project.config_dir() - } else { - project.data_dir() - }; - - Ok(format!("{}", path.display())) + Ok(project.config_dir().display().to_string()) } #[cfg(test)] From d47fe4f65a063a0fba0110c792587ac45be0c449 Mon Sep 17 00:00:00 2001 From: Ricky Hosfelt Date: Wed, 3 Sep 2025 20:38:46 -0400 Subject: [PATCH 3/4] Add a way to change the stragey by a function call --- Cargo.toml | 1 + src/lib.rs | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 160 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0868168..5d3dd1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ serde_yaml = { version = "0.9", optional = true } thiserror = "2.0" basic-toml = { version = "0.1.10", optional = true } toml = { version = "0.9", optional = true } +lazy_static = "1.5" [features] default = ["toml_conf"] diff --git a/src/lib.rs b/src/lib.rs index 5de8abd..99ecdc1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,13 +92,19 @@ //! [`basic_toml` crate]: https://docs.rs/basic_toml mod utils; +use etcetera::app_strategy; use utils::*; -use etcetera::{AppStrategy, AppStrategyArgs, app_strategy::choose_app_strategy}; +use etcetera::{ + AppStrategy, AppStrategyArgs, app_strategy::choose_app_strategy, + app_strategy::choose_native_strategy, +}; +use lazy_static::lazy_static; use serde::{Serialize, de::DeserializeOwned}; use std::fs::{self, File, OpenOptions, Permissions}; use std::io::{ErrorKind::NotFound, Write}; use std::path::{Path, PathBuf}; +use std::sync::Mutex; use thiserror::Error; #[cfg(feature = "toml_conf")] @@ -153,6 +159,10 @@ const EXTENSION: &str = "yml"; #[cfg(feature = "ron_conf")] const EXTENSION: &str = "ron"; +lazy_static! { + static ref STRATEGY: Mutex = Mutex::new(ConfigStrategy::App); +} + /// The errors the confy crate can encounter. #[derive(Debug, Error)] pub enum ConfyError { @@ -202,6 +212,84 @@ pub enum ConfyError { SetPermissionsFileError(#[source] std::io::Error), } +/// Determine what strategy `confy` should use +pub enum ConfigStrategy { + App, + Native, +} + +/// Change the strategy to use +/// +/// default is the AppStrategy +pub fn change_config_strategy(changer: ConfigStrategy) { + *STRATEGY + .lock() + .expect("Error getting lock on Config Stragey") = changer; +} + +enum InternalStrategy { + App(app_strategy::Xdg), + NativeMac(app_strategy::Apple), + NativeUnix(app_strategy::Unix), + NativeWindows(app_strategy::Windows), +} + +// we only every access the config dir function +impl AppStrategy for InternalStrategy { + fn home_dir(&self) -> &Path { + unimplemented!() + } + + fn config_dir(&self) -> PathBuf { + match self { + InternalStrategy::App(xdg) => xdg.config_dir(), + InternalStrategy::NativeMac(mac) => mac.config_dir(), + InternalStrategy::NativeUnix(unix) => unix.config_dir(), + InternalStrategy::NativeWindows(windows) => windows.config_dir(), + } + } + + fn data_dir(&self) -> PathBuf { + unimplemented!() + } + + fn cache_dir(&self) -> PathBuf { + unimplemented!() + } + + fn state_dir(&self) -> Option { + unimplemented!() + } + + fn runtime_dir(&self) -> Option { + unimplemented!() + } +} + +impl From for InternalStrategy { + fn from(value: app_strategy::Xdg) -> Self { + InternalStrategy::App(value) + } +} + +impl From for InternalStrategy { + fn from(value: app_strategy::Apple) -> Self { + InternalStrategy::NativeMac(value) + } +} + +impl From for InternalStrategy { + fn from(value: app_strategy::Unix) -> Self { + InternalStrategy::NativeUnix(value) + } +} + +impl From for InternalStrategy { + fn from(value: app_strategy::Windows) -> Self { + InternalStrategy::NativeWindows(value) + } +} + /// Load an application configuration from disk /// /// A new configuration file is created with default values if none @@ -469,14 +557,29 @@ pub fn get_configuration_file_path<'a>( config_name: impl Into>, ) -> Result { let config_name = config_name.into().unwrap_or("default-config"); - let project = choose_app_strategy(AppStrategyArgs { - top_level_domain: "rs".to_string(), - author: "".to_string(), - app_name: app_name.to_string(), - }) - .map_err(|e| { - ConfyError::BadConfigDirectory(format!("could not determine home directory path: {e}")) - })?; + let project: InternalStrategy = match *STRATEGY + .lock() + .expect("Error getting lock on config strategy") + { + ConfigStrategy::App => choose_app_strategy(AppStrategyArgs { + top_level_domain: "rs".to_string(), + author: "".to_string(), + app_name: app_name.to_string(), + }) + .map_err(|e| { + ConfyError::BadConfigDirectory(format!("could not determine home directory path: {e}")) + })? + .into(), + ConfigStrategy::Native => choose_native_strategy(AppStrategyArgs { + top_level_domain: "rs".to_string(), + author: "".to_string(), + app_name: app_name.to_string(), + }) + .map_err(|e| { + ConfyError::BadConfigDirectory(format!("could not determine home directory path: {e}")) + })? + .into(), + }; let config_dir_str = get_configuration_directory_str(&project)?; @@ -572,6 +675,53 @@ mod tests { }) } + #[test] + fn test_store_path_native() { + // change the strategy first then the app will always use it + change_config_strategy(ConfigStrategy::Native); + + with_config_path(|path| { + let config: ExampleConfig = ExampleConfig { + name: "Test".to_string(), + count: 42, + }; + + let file_path = get_configuration_file_path("example-app", "example-config").unwrap(); + + if cfg!(target_os = "macos") { + assert_eq!( + file_path, + Path::new(&format!( + "{}/Library/Preferences/rs.example-app/example-config.toml", + std::env::home_dir().unwrap().display() + )), + ); + } else if cfg!(target_os = "linux") { + assert_eq!( + file_path, + Path::new(&format!( + "{}/.config/example-app/example-config.toml", + std::env::home_dir().unwrap().display() + )) + ); + } else { + //windows + assert_eq!( + file_path, + Path::new(&format!( + "{}\\AppData\\Roaming\\example-app\\config\\example-config.toml", + std::env::home_dir().unwrap().display() + )), + ); + } + + // Make sure it is still the same config file + store_path(path, &config).expect("store_path failed"); + let loaded = load_path(path).expect("load_path failed"); + assert_eq!(config, loaded); + }) + } + /// [`store_path_perms`] stores [`ExampleConfig`], with only read permission for owner (UNIX). #[test] #[cfg(unix)] From 417059048ce02016f08edab1d791200f49905da6 Mon Sep 17 00:00:00 2001 From: Ricky Hosfelt Date: Wed, 3 Sep 2025 21:59:55 -0400 Subject: [PATCH 4/4] Add a change strategy example --- src/lib.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 99ecdc1..6edddc5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -722,6 +722,85 @@ mod tests { }) } + #[test] + fn test_store_path_change() { + // change the strategy first to native + change_config_strategy(ConfigStrategy::Native); + + with_config_path(|path| { + let config: ExampleConfig = ExampleConfig { + name: "Test".to_string(), + count: 42, + }; + + let file_path = get_configuration_file_path("example-app", "example-config").unwrap(); + + if cfg!(target_os = "macos") { + assert_eq!( + file_path, + Path::new(&format!( + "{}/Library/Preferences/rs.example-app/example-config.toml", + std::env::home_dir().unwrap().display() + )), + ); + } else if cfg!(target_os = "linux") { + assert_eq!( + file_path, + Path::new(&format!( + "{}/.config/example-app/example-config.toml", + std::env::home_dir().unwrap().display() + )) + ); + } else { + //windows + assert_eq!( + file_path, + Path::new(&format!( + "{}\\AppData\\Roaming\\example-app\\config\\example-config.toml", + std::env::home_dir().unwrap().display() + )), + ); + } + + //change the strategy back to Application style + change_config_strategy(ConfigStrategy::App); + + let file_path = get_configuration_file_path("example-app", "example-config").unwrap(); + + if cfg!(target_os = "macos") { + assert_eq!( + file_path, + Path::new(&format!( + "{}/.config/example-app/example-config.toml", + std::env::home_dir().unwrap().display() + )), + ); + } else if cfg!(target_os = "linux") { + assert_eq!( + file_path, + Path::new(&format!( + "{}/.config/example-app/example-config.toml", + std::env::home_dir().unwrap().display() + )) + ); + } else { + //windows + assert_eq!( + file_path, + Path::new(&format!( + "{}\\AppData\\Roaming\\example-app\\config\\example-config.toml", + std::env::home_dir().unwrap().display() + )), + ); + } + + // Make sure it is still the same config file + store_path(path, &config).expect("store_path failed"); + let loaded = load_path(path).expect("load_path failed"); + assert_eq!(config, loaded); + }) + } + /// [`store_path_perms`] stores [`ExampleConfig`], with only read permission for owner (UNIX). #[test] #[cfg(unix)]