1#![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 pub static ref SYSTEM_CONFIG_DIRS: Option<ProjectDirs> =
55 ProjectDirs::from("com", "emresult", "xvc");
56
57 pub static ref USER_CONFIG_DIRS: Option<BaseDirs> = BaseDirs::new();
60
61 pub static ref USER_DIRS: Option<UserDirs> = UserDirs::new();
64}
65
66#[derive(Debug, Copy, Clone, EnumString, EnumDisplay, IntoStaticStr, Serialize, Deserialize)]
68#[strum(serialize_all = "lowercase")]
69pub enum XvcConfigOptionSource {
70 Default,
72 System,
74 Global,
76 Project,
78 Local,
80 CommandLine,
82 Environment,
84 Runtime,
86}
87
88#[derive(Debug, Copy, Clone)]
90pub struct XvcConfigOption<T> {
91 pub source: XvcConfigOptionSource,
93 pub option: T,
95}
96
97#[derive(Debug, Copy, Clone, EnumString, EnumDisplay, IntoStaticStr)]
99pub enum XvcVerbosity {
100 #[strum(serialize = "quiet", serialize = "0")]
102 Quiet,
103 #[strum(serialize = "default", serialize = "error", serialize = "1")]
105 Default,
106 #[strum(serialize = "warn", serialize = "2")]
108 Warn,
109 #[strum(serialize = "info", serialize = "3")]
111 Info,
112 #[strum(serialize = "debug", serialize = "4")]
114 Debug,
115 #[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#[derive(Debug, Clone)]
135pub struct XvcConfigValue {
136 pub source: XvcConfigOptionSource,
138 pub value: TomlValue,
140}
141
142impl XvcConfigValue {
143 pub fn new(source: XvcConfigOptionSource, value: TomlValue) -> Self {
145 Self { source, value }
146 }
147}
148
149#[derive(Debug, Clone)]
151pub struct XvcConfigMap {
152 pub source: XvcConfigOptionSource,
154 pub map: HashMap<String, TomlValue>,
156}
157
158#[derive(Debug, Clone)]
164pub struct XvcConfig {
165 pub current_dir: XvcConfigOption<AbsolutePath>,
167 pub config_maps: Vec<XvcConfigMap>,
169 pub the_config: HashMap<String, XvcConfigValue>,
171 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn current_dir(&self) -> Result<&AbsolutePath> {
522 let pb = &self.current_dir.option;
523 Ok(pb)
524 }
525
526 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 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 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
576pub trait UpdateFromXvcConfig {
580 fn update_from_conf(self, conf: &XvcConfig) -> Result<Box<Self>>;
585}
586
587pub trait FromConfigKey<T: FromStr> {
595 fn from_conf(conf: &XvcConfig) -> T;
598
599 fn try_from_conf(conf: &XvcConfig) -> Result<T>;
602}
603
604#[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
622pub 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}