xvc_config/
lib.rs

1//! Provides a general solution to maintain configuration spanned across different sources.
2//!
3//!
4//! - Default Values
5//! - System configuration
6//! - User configuration
7//! - Public project configuration (tracked by Git)
8//! - Private (local) project configuration (not tracked by Git)
9//! - Environment variables
10//! - Command line options
11//!
12//!
13//! The configuration keys are string.
14//! Configuration values can be:
15//! - string
16//! - bool
17//! - int
18//! - float
19//!
20//! Configuration files are in TOML.
21//!
22//! Options can be nested like `group.name = value`.
23//!
24//! Each option can be tracked to its source via [XvcConfigOption].
25//!
26#![warn(missing_docs)]
27#![forbid(unsafe_code)]
28pub mod config_params;
29pub mod error;
30
31pub use config_params::XvcConfigParams;
32
33use directories_next::{BaseDirs, ProjectDirs, UserDirs};
34use lazy_static::lazy_static;
35use regex::Regex;
36use serde::{Deserialize, Serialize};
37use std::{
38    collections::HashMap,
39    fmt, fs,
40    path::{Path, PathBuf},
41    str::FromStr,
42};
43use xvc_logging::debug;
44use xvc_walker::AbsolutePath;
45
46use strum_macros::{Display as EnumDisplay, EnumString, IntoStaticStr};
47
48use crate::error::{Error, Result};
49use toml::Value as TomlValue;
50
51lazy_static! {
52    /// System specific configuration directory.
53    /// see [directories_next::ProjectDirs].
54    pub static ref SYSTEM_CONFIG_DIRS: Option<ProjectDirs> =
55        ProjectDirs::from("com", "emresult", "xvc");
56
57    /// User configuration directories.
58    /// See [directories_next::BaseDirs].
59    pub static ref USER_CONFIG_DIRS: Option<BaseDirs> = BaseDirs::new();
60
61    /// User directories.
62    /// see [directories_next::UserDirs].
63    pub static ref USER_DIRS: Option<UserDirs> = UserDirs::new();
64}
65
66/// Define the source where an option is obtained
67#[derive(Debug, Copy, Clone, EnumString, EnumDisplay, IntoStaticStr, Serialize, Deserialize)]
68#[strum(serialize_all = "lowercase")]
69pub enum XvcConfigOptionSource {
70    /// Default value defined in source code
71    Default,
72    /// System-wide configuration value from [SYSTEM_CONFIG_DIRS]
73    System,
74    /// User's configuration value from [USER_CONFIG_DIRS]
75    Global,
76    /// Project specific configuration that can be shared
77    Project,
78    /// Project specific configuration that's not meant to be shared (personal/local)
79    Local,
80    /// Options obtained from the command line
81    CommandLine,
82    /// Options from environment variables
83    Environment,
84    /// Options set while running the software, automatically.
85    Runtime,
86}
87
88/// The option and its source.
89#[derive(Debug, Copy, Clone)]
90pub struct XvcConfigOption<T> {
91    /// Where did we get this option?
92    pub source: XvcConfigOptionSource,
93    /// The key and value
94    pub option: T,
95}
96
97/// Verbosity levels for Xvc CLI
98#[derive(Debug, Copy, Clone, EnumString, EnumDisplay, IntoStaticStr)]
99pub enum XvcVerbosity {
100    /// Do not print anything
101    #[strum(serialize = "quiet", serialize = "0")]
102    Quiet,
103    /// Print default output and errors
104    #[strum(serialize = "default", serialize = "error", serialize = "1")]
105    Default,
106    /// Print default output, warnings and errors
107    #[strum(serialize = "warn", serialize = "2")]
108    Warn,
109    /// Print default output, info, warnings and errors
110    #[strum(serialize = "info", serialize = "3")]
111    Info,
112    /// Print default output, errors, warnings, info and debug output
113    #[strum(serialize = "debug", serialize = "4")]
114    Debug,
115    /// Print default output, errors, warnings, info, debug and tracing output
116    #[strum(serialize = "trace", serialize = "5")]
117    Trace,
118}
119
120impl From<u8> for XvcVerbosity {
121    fn from(v: u8) -> Self {
122        match v {
123            0 => Self::Quiet,
124            1 => Self::Default,
125            2 => Self::Warn,
126            3 => Self::Info,
127            4 => Self::Debug,
128            _ => Self::Trace,
129        }
130    }
131}
132
133/// A configuration value with its source
134#[derive(Debug, Clone)]
135pub struct XvcConfigValue {
136    /// Where did we get this value?
137    pub source: XvcConfigOptionSource,
138    /// The value itself
139    pub value: TomlValue,
140}
141
142impl XvcConfigValue {
143    /// Create a new XvcConfigValue
144    pub fn new(source: XvcConfigOptionSource, value: TomlValue) -> Self {
145        Self { source, value }
146    }
147}
148
149/// A set of options defined as a TOML document from a single [XvcConfigOptionSource]
150#[derive(Debug, Clone)]
151pub struct XvcConfigMap {
152    /// Where does these option come from?
153    pub source: XvcConfigOptionSource,
154    /// The key-value map for the options
155    pub map: HashMap<String, TomlValue>,
156}
157
158/// Keeps track of all Xvc configuration.
159///
160/// It's created by [XvcRoot] using the options from [XvcConfigInitParams].
161/// Keeps the current directory, that can also be set manually from the command line.
162/// It loads several config maps (one for each [XvcConfigOptionSource]) and cascadingly merges them to get an actual configuration.
163#[derive(Debug, Clone)]
164pub struct XvcConfig {
165    /// Current directory. It can be set with xvc -C option
166    pub current_dir: XvcConfigOption<AbsolutePath>,
167    /// Configuration values for each level
168    pub config_maps: Vec<XvcConfigMap>,
169    /// The current configuration map, updated cascadingly
170    pub the_config: HashMap<String, XvcConfigValue>,
171    /// The init params used to create this config
172    pub init_params: XvcConfigParams,
173}
174
175impl fmt::Display for XvcConfig {
176    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
177        writeln!(f, "\nCurrent Configuration")?;
178        writeln!(
179            f,
180            "current_dir: {:?} ({:?})",
181            self.current_dir.option, self.current_dir.source
182        )?;
183        for (k, v) in &self.the_config {
184            writeln!(f, "{}: {} ({})", k, v.value, v.source)?;
185        }
186        writeln!(f)
187    }
188}
189
190impl XvcConfig {
191    /// Loads the default configuration from `p`.
192    ///
193    /// The configuration must be a valid TOML document.
194    fn default_conf(p: &XvcConfigParams) -> Self {
195        let default_conf = p
196            .default_configuration
197            .parse::<TomlValue>()
198            .expect("Error in default configuration!");
199        let hm = toml_value_to_hashmap("".into(), default_conf);
200        let hm_for_list = hm.clone();
201        let the_config: HashMap<String, XvcConfigValue> = hm
202            .into_iter()
203            .map(|(k, v)| {
204                (
205                    k,
206                    XvcConfigValue {
207                        source: XvcConfigOptionSource::Default,
208                        value: v,
209                    },
210                )
211            })
212            .collect();
213
214        XvcConfig {
215            current_dir: XvcConfigOption {
216                option: std::env::current_dir()
217                    .expect("Cannot determine current directory")
218                    .into(),
219                source: XvcConfigOptionSource::Default,
220            },
221            the_config,
222            config_maps: vec![XvcConfigMap {
223                map: hm_for_list,
224                source: XvcConfigOptionSource::Default,
225            }],
226            init_params: p.clone(),
227        }
228    }
229
230    /// Returns string value for `key`.
231    ///
232    /// The value is parsed from the corresponding TomlValue stored in [`self.the_config`].
233    pub fn get_str(&self, key: &str) -> Result<XvcConfigOption<String>> {
234        let opt = self.get_toml_value(key)?;
235        if let TomlValue::String(val) = opt.option {
236            Ok(XvcConfigOption::<String> {
237                option: val,
238                source: opt.source,
239            })
240        } else {
241            Err(Error::MismatchedValueType { key: key.into() })
242        }
243    }
244
245    /// Returns bool value for `key`.
246    ///
247    /// The value is parsed from the corresponding TomlValue stored in [`self.the_config`].
248    pub fn get_bool(&self, key: &str) -> Result<XvcConfigOption<bool>> {
249        let opt = self.get_toml_value(key)?;
250        if let TomlValue::Boolean(val) = opt.option {
251            Ok(XvcConfigOption::<bool> {
252                option: val,
253                source: opt.source,
254            })
255        } else {
256            Err(Error::MismatchedValueType { key: key.into() })
257        }
258    }
259
260    /// Returns int value for `key`.
261    ///
262    /// The value is parsed from the corresponding TomlValue stored in [`self.the_config`].
263    pub fn get_int(&self, key: &str) -> Result<XvcConfigOption<i64>> {
264        let opt = self.get_toml_value(key)?;
265        if let TomlValue::Integer(val) = opt.option {
266            Ok(XvcConfigOption::<i64> {
267                option: val,
268                source: opt.source,
269            })
270        } else {
271            Err(Error::MismatchedValueType { key: key.into() })
272        }
273    }
274
275    /// Returns float value for `key`.
276    ///
277    /// The value is parsed from the corresponding TomlValue stored in [`self.the_config`].
278    pub fn get_float(&self, key: &str) -> Result<XvcConfigOption<f64>> {
279        let opt = self.get_toml_value(key)?;
280        if let TomlValue::Float(val) = opt.option {
281            Ok(XvcConfigOption::<f64> {
282                option: val,
283                source: opt.source,
284            })
285        } else {
286            Err(Error::MismatchedValueType { key: key.into() })
287        }
288    }
289
290    /// Returns [TOML value][TomlValue] corresponding to key.
291    ///
292    /// It's returned _without parsing_ from [`self.the_config`]
293    pub fn get_toml_value(&self, key: &str) -> Result<XvcConfigOption<TomlValue>> {
294        let value = self
295            .the_config
296            .get(key)
297            .ok_or(Error::ConfigKeyNotFound { key: key.into() })?
298            .to_owned();
299
300        Ok(XvcConfigOption::<TomlValue> {
301            option: value.value,
302            source: value.source,
303        })
304    }
305
306    /// Updates [`self.the_config`]  with the values found in `new_map`.
307    ///
308    /// The configuration source for all values in `new_map` is set to be `new_source`.
309    fn update_from_hash_map(
310        &self,
311        new_map: HashMap<String, TomlValue>,
312        new_source: XvcConfigOptionSource,
313    ) -> Result<Self> {
314        let mut current_map = self.the_config.clone();
315        new_map.iter().for_each(|(k, v)| {
316            current_map.insert(
317                k.clone(),
318                XvcConfigValue {
319                    source: new_source,
320                    value: v.clone(),
321                },
322            );
323        });
324
325        let mut new_config_maps = self.config_maps.clone();
326        new_config_maps.push(XvcConfigMap {
327            source: new_source,
328            map: new_map,
329        });
330
331        Ok(Self {
332            current_dir: self.current_dir.clone(),
333            init_params: self.init_params.clone(),
334            the_config: current_map,
335            config_maps: new_config_maps,
336        })
337    }
338
339    /// Updates [`self.the_config`] after parsing `configuration`.
340    ///
341    /// `configuration` must be a valid TOML document.
342    /// [Source][XvcConfigOptionSource] of all read values are set to `new_source`.
343    fn update_from_toml(
344        &self,
345        configuration: String,
346        new_source: XvcConfigOptionSource,
347    ) -> Result<Self> {
348        let new_val = configuration.parse::<TomlValue>()?;
349        let key = "".to_string();
350        let new_map = toml_value_to_hashmap(key, new_val);
351        self.update_from_hash_map(new_map, new_source)
352    }
353
354    /// Reads `file_name` and calls `self.update_from_toml` with the contents.
355    fn update_from_file(
356        &self,
357        file_name: &Path,
358        source: XvcConfigOptionSource,
359    ) -> Result<XvcConfig> {
360        if file_name.is_file() {
361            let config_string =
362                fs::read_to_string(file_name).map_err(|source| Error::IoError { source })?;
363            self.update_from_toml(config_string, source)
364        } else {
365            Err(Error::ConfigurationForSourceNotFound {
366                config_source: source.to_string(),
367                path: file_name.as_os_str().into(),
368            })
369        }
370    }
371
372    /// Return the system configuration file path for Xvc
373    pub fn system_config_file() -> Result<PathBuf> {
374        Ok(SYSTEM_CONFIG_DIRS
375            .to_owned()
376            .ok_or(Error::CannotDetermineSystemConfigurationPath)?
377            .config_dir()
378            .to_path_buf())
379    }
380
381    /// Return the user configuration file path for Xvc
382    pub fn user_config_file() -> Result<PathBuf> {
383        Ok(USER_CONFIG_DIRS
384            .to_owned()
385            .ok_or(Error::CannotDetermineUserConfigurationPath)?
386            .config_dir()
387            .join("xvc"))
388    }
389
390    /// Load all keys from the environment that starts with `XVC_` and build a hash map with them.
391    ///
392    /// The resulting hash map has `key: value` elements for environment variables in the form `XVC_key=value`.
393    fn env_map() -> Result<HashMap<String, TomlValue>> {
394        let mut hm = HashMap::<String, String>::new();
395        let env_key_re = Regex::new(r"^XVC_?(.+)")?;
396        for (k, v) in std::env::vars() {
397            if let Some(cap) = env_key_re.captures(&k) {
398                hm.insert(cap[1].to_owned(), v);
399            }
400        }
401
402        // Try to parse the values:
403        // bool -> i64 -> f64 -> String
404
405        let hm_val = hm
406            .into_iter()
407            .map(|(k, v)| (k, Self::parse_to_value(v)))
408            .collect();
409
410        Ok(hm_val)
411    }
412
413    /// Parses a string to most specific type that can represent it.
414    ///
415    /// The parsing order is
416    ///
417    /// bool -> int -> float -> string.
418    ///
419    /// If it's not parsed as bool, int is tried, then float.
420    /// If none of these work, return it as String.
421    /// This is used in [self.env_map] to get TOML values from environment variables.
422    /// Other documents in TOML form are using native TOML parsing.
423    fn parse_to_value(v: String) -> TomlValue {
424        if let Ok(b) = v.parse::<bool>() {
425            TomlValue::Boolean(b)
426        } else if let Ok(i) = v.parse::<i64>() {
427            TomlValue::Integer(i)
428        } else if let Ok(f) = v.parse::<f64>() {
429            TomlValue::Float(f)
430        } else {
431            TomlValue::String(v)
432        }
433    }
434
435    /// Parses a vector of strings, and returns a `Vec<(key, value)>`.
436    fn parse_key_value_vector(vector: Vec<String>) -> Vec<(String, TomlValue)> {
437        vector
438            .into_iter()
439            .map(|str| {
440                let elements: Vec<&str> = str.split('=').collect();
441                let key = elements[0].trim().to_owned();
442                let value = Self::parse_to_value(elements[1].trim().to_owned());
443                (key, value)
444            })
445            .collect()
446    }
447
448    /// Loads all config files
449    /// Overrides all options with the given key=value style options in the
450    /// command line
451    pub fn new(p: XvcConfigParams) -> Result<XvcConfig> {
452        let mut config = XvcConfig::default_conf(&p);
453
454        config.current_dir = XvcConfigOption {
455            option: p.current_dir,
456            source: XvcConfigOptionSource::Runtime,
457        };
458
459        let mut update = |source, file: std::result::Result<&Path, &Error>| match file {
460            Ok(config_file) => match config.update_from_file(config_file, source) {
461                Ok(new_config) => config = new_config,
462                Err(err) => {
463                    err.debug();
464                }
465            },
466            Err(err) => {
467                debug!("{}", err);
468            }
469        };
470
471        if p.include_system_config {
472            let f = Self::system_config_file();
473            update(XvcConfigOptionSource::System, f.as_deref());
474        }
475
476        if p.include_user_config {
477            update(
478                XvcConfigOptionSource::Global,
479                Self::user_config_file().as_deref(),
480            );
481        }
482
483        if let Some(project_config_path) = p.project_config_path {
484            update(XvcConfigOptionSource::Project, Ok(&project_config_path));
485        }
486
487        if let Some(local_config_path) = p.local_config_path {
488            update(XvcConfigOptionSource::Local, Ok(&local_config_path));
489        }
490
491        if p.include_environment_config {
492            let env_config = Self::env_map().unwrap();
493            match config.update_from_hash_map(env_config, XvcConfigOptionSource::Environment) {
494                Ok(conf) => config = conf,
495                Err(err) => {
496                    err.debug();
497                }
498            }
499        }
500
501        if let Some(cli_config) = p.command_line_config {
502            let map: HashMap<String, TomlValue> = Self::parse_key_value_vector(cli_config)
503                .into_iter()
504                .collect();
505            match config.update_from_hash_map(map, XvcConfigOptionSource::CommandLine) {
506                Ok(conf) => {
507                    config = conf;
508                }
509                Err(err) => {
510                    err.debug();
511                }
512            }
513        }
514
515        Ok(config)
516    }
517
518    /// Where do we run the command?
519    ///
520    /// This can be modified by options in the command line, so it's not always equal to [std::env::current_dir()]
521    pub fn current_dir(&self) -> Result<&AbsolutePath> {
522        let pb = &self.current_dir.option;
523        Ok(pb)
524    }
525
526    /// Globally Unique Identified for the Xvc Repository / Project
527    ///
528    /// It's stored in `core.guid` option.
529    /// It's created in [`XvcRoot::init`] and shouldn't be tampered with.
530    /// Storage commands use this to create different paths for different Xvc projects.
531    pub fn guid(&self) -> Option<String> {
532        match self.get_str("core.guid") {
533            Ok(opt) => Some(opt.option),
534            Err(err) => {
535                err.warn();
536                None
537            }
538        }
539    }
540
541    /// The current verbosity level.
542    /// Set with `core.verbosity` option.
543    pub fn verbosity(&self) -> XvcVerbosity {
544        let verbosity_str = self.get_str("core.verbosity");
545        let verbosity_str = match verbosity_str {
546            Ok(opt) => opt.option,
547            Err(err) => {
548                err.warn();
549                "1".to_owned()
550            }
551        };
552
553        match XvcVerbosity::from_str(&verbosity_str) {
554            Ok(v) => v,
555            Err(source) => {
556                Error::StrumError { source }.warn();
557                XvcVerbosity::Default
558            }
559        }
560    }
561
562    /// Returns a struct (`T`) value by using its `FromStr` implementation.
563    /// It parses the string to get the value.
564    pub fn get_val<T>(&self, key: &str) -> Result<T>
565    where
566        T: FromStr,
567    {
568        let str_val = self.get_str(key)?;
569        let val: T = T::from_str(&str_val.option).map_err(|_| Error::EnumTypeConversionError {
570            cause_key: key.to_owned(),
571        })?;
572        Ok(val)
573    }
574}
575
576/// Trait to update CLI options with defaults from configuration.
577///
578/// When a CLI struct like [xvc_pipeline::PipelineCLI] implements this trait, it reads the configuration and updates values not set in the command line accordingly.
579pub trait UpdateFromXvcConfig {
580    /// Update the implementing struct from the configuration.
581    /// Reading the relevant keys and values of the config is in implementor's responsibility.
582    ///
583    /// This is used to abstract away CLI structs and crate options.
584    fn update_from_conf(self, conf: &XvcConfig) -> Result<Box<Self>>;
585}
586
587/// A struct implementing this trait can instantiate itself from XvcConfig.
588///
589/// When an option should be parsed and converted to a struct, it implements this trait.
590/// The functions are basically identical, and uses [XvcConfig::get_val] to instantiate.
591/// It's used to bind a configuration key (str) "group.key" with a struct.
592///
593/// See [conf] macro below for a shortcut.
594pub trait FromConfigKey<T: FromStr> {
595    /// Create a value of type `T` from configuration.
596    /// Supposed to panic! if there is no key, or the value cannot be parsed.
597    fn from_conf(conf: &XvcConfig) -> T;
598
599    /// Try to create a type `T` from the configuration.
600    /// Returns error if there is no key, or the value cannot be parsed.
601    fn try_from_conf(conf: &XvcConfig) -> Result<T>;
602}
603
604/// Binds a type with a configuration key.
605///
606/// When you declare `conf!("group.subgroup.key", MyType)`, this macro writes the code necessary to create `MyType` from the configuration.
607#[macro_export]
608macro_rules! conf {
609    ($type: ty, $key: literal) => {
610        impl FromConfigKey<$type> for $type {
611            fn from_conf(conf: &$crate::XvcConfig) -> $type {
612                conf.get_val::<$type>($key).unwrap()
613            }
614
615            fn try_from_conf(conf: &$crate::XvcConfig) -> $crate::error::Result<$type> {
616                conf.get_val::<$type>($key)
617            }
618        }
619    };
620}
621
622/// Convert a TomlValue which can be a [TomlValue::Table] or any other simple type to a hash map with keys in the hierarchical form.
623///
624/// A `key` in TOML table `[group]` will have `group.key` in the returned hash map.
625/// The groups can be arbitrarily deep.
626pub fn toml_value_to_hashmap(key: String, value: TomlValue) -> HashMap<String, TomlValue> {
627    let mut key_value_stack = Vec::<(String, TomlValue)>::new();
628    let mut key_value_map = HashMap::<String, TomlValue>::new();
629    key_value_stack.push((key, value));
630    while let Some((key, value)) = key_value_stack.pop() {
631        match value {
632            TomlValue::Table(t) => {
633                for (subkey, subvalue) in t {
634                    if key.is_empty() {
635                        key_value_stack.push((subkey, subvalue));
636                    } else {
637                        key_value_stack.push((format!("{}.{}", key, subkey), subvalue));
638                    }
639                }
640            }
641            _ => {
642                key_value_map.insert(key, value);
643            }
644        }
645    }
646    key_value_map
647}
648
649#[cfg(test)]
650mod tests {
651
652    use super::*;
653    use crate::error::Result;
654    use log::LevelFilter;
655    use toml::Value as TomlValue;
656    use xvc_logging::setup_logging;
657
658    pub fn test_logging(level: LevelFilter) {
659        setup_logging(Some(level), Some(LevelFilter::Trace));
660    }
661
662    #[test]
663    fn test_toml_value_to_hashmap() -> Result<()> {
664        test_logging(LevelFilter::Trace);
665        let str_value = "foo = 'bar'".parse::<TomlValue>()?;
666        let str_hm = toml_value_to_hashmap("".to_owned(), str_value);
667
668        assert!(str_hm["foo"] == TomlValue::String("bar".to_string()));
669
670        let table_value = r#"[core]
671        foo = "bar"
672        val = 100
673        "#
674        .parse::<TomlValue>()?;
675
676        let table_hm = toml_value_to_hashmap("".to_owned(), table_value);
677        assert!(table_hm.get("core.foo") == Some(&TomlValue::String("bar".to_string())));
678        Ok(())
679    }
680}