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
use super::*; use std::fs; use std::path::PathBuf; /// A Configurable type that loads from the equivalent of `$XDG_DATA_HOME` pub trait Data: Configurable { /// Ensures the directory exists fn ensure_dir() -> Result<PathBuf, Error> { let (qualifier, org, app) = (Self::QUALIFIER, Self::ORGANIZATION, Self::APPLICATION); let dirs = directories::ProjectDirs::from(qualifier, org, app) .expect("system must have a valid $HOME directory"); let dirs = dirs.data_dir(); fs::create_dir_all(&dirs).map_err(Error::Write)?; Ok(dirs.to_owned()) } } /// A Configurable type that loads from the equivalent of `$XDG_CONFIG_HOME` pub trait Config: Configurable { /// Ensures the directory exists fn ensure_dir() -> Result<PathBuf, Error> { let (qualifier, org, app) = (Self::QUALIFIER, Self::ORGANIZATION, Self::APPLICATION); let dirs = directories::ProjectDirs::from(qualifier, org, app) .expect("system must have a valid $HOME directory"); let dirs = dirs.config_dir(); fs::create_dir_all(&dirs).map_err(Error::Write)?; Ok(dirs.to_owned()) } } /// Trait to provide easier loaded/saving of a `config` type /// /// Provide static strs for `QUALIFIER`, `ORGANIZATION`, `APPLICATION` and `NAME` /// /// Will which will produce $CONFIG_PATH/qualifier.organization.application/name /// /// # Configuration-style configs (e.g. stuff that should be human editable) /// ``` /// use serde::{Serialize, Deserialize}; /// use std::path::PathBuf; /// use configurable::{Config, Data, Configurable, Error}; /// /// // Default is required /// #[derive(Default, Serialize, Deserialize)] /// struct MyConfig; /// /// // For configurations (e.g. foo.toml) /// impl Config for MyConfig {}; /// impl Configurable for MyConfig { /// const ORGANIZATION: &'static str = "museun"; /// const APPLICATION: &'static str = "foobar"; /// const NAME: &'static str = "config.toml"; /// /// fn ensure_dir() -> Result<PathBuf, Error> { /// // Config `configs` /// <Self as Config>::ensure_dir() /// } /// } /// // will place it here: /// // -> "~/.config/com.github/museun/foobar/config.toml /// ``` /// /// # Data-style configurations (e.g. formats outside of toml) /// ``` /// use serde::{Serialize, Deserialize}; /// use std::path::PathBuf; /// use configurable::{Config, Data, Configurable, Error}; /// /// // Default is required /// #[derive(Default, Serialize, Deserialize)] /// struct MyMap { map: std::collections::HashMap<String,i32> } /// /// // For configurations (e.g. foo.toml) /// impl Data for MyMap {}; /// impl Configurable for MyMap { /// const ORGANIZATION: &'static str = "museun"; /// const APPLICATION: &'static str = "foobar"; /// const NAME: &'static str = "mapping.json"; /// /// fn ensure_dir() -> Result<PathBuf, Error> { /// // Data `configs` /// <Self as Data>::ensure_dir() /// } /// } /// // will place it here: /// // -> "~/.local/share/com.github/museun/foobar/mapping.json /// ```` pub trait Configurable: Default + serde::Serialize + serde::de::DeserializeOwned { /// Qualifier (e.g. "com.github") /// /// Defaults to `com.github` const QUALIFIER: &'static str = "com.github"; /// Organization (e.g. "museun" (in github.com/museun)) /// /// You must provide this const ORGANIZATION: &'static str; /// Application (e.g. "foo" (in github.com/museun/foo)) /// /// You must provide this const APPLICATION: &'static str; /// The name of the toml file, with extension /// /// ex: `config.toml` const NAME: &'static str; /// Ensures the directory exists /// /// Implement either `Config` or `Data` /// then delegate to it /// /// ``` /// # use serde::{Serialize, Deserialize}; /// # use std::path::PathBuf; /// # use configurable::{Config, Configurable, Error}; /// # #[derive(Default, Serialize, Deserialize)] /// # struct Foo; /// // Config or Data /// impl Config for Foo {}; /// impl Configurable for Foo { /// const ORGANIZATION: &'static str = "some_org"; /// const APPLICATION: &'static str = "foobar"; /// const NAME: &'static str = "config.toml"; /// /// fn ensure_dir() -> Result<PathBuf, Error> { /// // Config or Data /// <Self as Config>::ensure_dir() /// } /// } /// ``` fn ensure_dir() -> Result<PathBuf, Error>; /// Loads, or defaults the configuration /// /// Returns a `LoadState` /// * Default meant it created a default instance /// * Loaded meant it created the instance from the file fn load_or_default() -> Result<LoadState<Self>, Error> { match Self::load() { Ok(this) => Ok(LoadState::Loaded(this)), Err(Error::Read(..)) => Ok(LoadState::Default(Self::default())), Err(err) => Err(err), } } /// Tries to load the configuration fn load() -> Result<Self, Error> { let dir = Self::ensure_dir()?.join(Self::NAME); let data = fs::read_to_string(dir).map_err(Error::Read)?; toml::from_str(&data).map_err(Error::TomlRead) } /// Tries to save the configuration fn save(&self) -> Result<(), Error> { let dir = Self::ensure_dir()?.join(Self::NAME); let s = toml::to_string_pretty(&self).map_err(Error::TomlWrite)?; fs::write(dir, s).map_err(Error::Write) } /// Tries to dump the config to the writer fn dump(&self, mut out: impl std::io::Write) -> Result<(), Error> { let s = toml::to_string_pretty(&self).map_err(Error::TomlWrite)?; out.write_all(s.as_bytes()).map_err(Error::Write) } /// Ensures the directory exists and returns a `PathBuf` to it fn dir() -> Result<PathBuf, Error> { Self::ensure_dir() } /// Ensures the directory exists and returns a `PathBuf` to the /// configuration file inside of the directory fn path() -> Result<PathBuf, Error> { Self::ensure_dir().map(|d| d.join(Self::NAME)) } }