dotfiles_processor/
context.rs

1// Copyright (c) 2021-2022 Miguel Barreto and others
2//
3// Permission is hereby granted, free of charge, to any person obtaining
4// a copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to
8// permit persons to whom the Software is furnished to do so, subject to
9// the following conditions:
10//
11// The above copyright notice and this permission notice shall be
12// included in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22use std::{
23  collections::HashMap,
24  convert::TryFrom,
25  path::{Path, PathBuf},
26};
27
28use dotfiles_core::{
29  error::{process_until_first_err, DotfilesError},
30  path::convert_path_to_absolute,
31  yaml_util::{fold_hash_until_first_err, map_yaml_array, read_yaml_file},
32  Setting, Settings,
33};
34use getset::Getters;
35use strict_yaml_rust::StrictYaml;
36
37use crate::known_directive::{KnownAction, KnownDirective};
38
39/// A context represents an environment in which defaults can be overriden, it can be thought of as
40/// the context of an individual configuration file to apply.
41///
42/// Notice that contexts *can* be built on top of one another, so that defaults can be overriden
43/// multiple times, and thus have some sort of configuration inheritance.
44#[derive(Getters)]
45pub struct Context {
46  /// The default overrides for the current context.
47  #[getset(get = "pub")]
48  defaults: HashMap<String, Settings>,
49  /// The list of actions parsed from this file.
50  #[getset(get = "pub")]
51  actions: Vec<KnownAction<'static>>,
52  /// The absolute path to the file to which this context corresponds.
53  #[getset(get = "pub")]
54  file: PathBuf,
55}
56
57impl TryFrom<&str> for Context {
58  type Error = DotfilesError;
59  fn try_from(file_name: &str) -> Result<Self, Self::Error> {
60    log::debug!("creating context for {:?}", file_name);
61    let absolute_file = convert_path_to_absolute(&PathBuf::from(file_name), None)?;
62    log::debug!("Absolute file name: {:?}", absolute_file.to_str());
63
64    Ok(Self {
65      defaults: Default::default(),
66      actions: Default::default(),
67      file: absolute_file,
68    })
69  }
70}
71
72impl TryFrom<&Path> for Context {
73  type Error = DotfilesError;
74  fn try_from(file_name: &Path) -> Result<Self, Self::Error> {
75    log::debug!("creating context for {:?}", file_name.to_str().unwrap());
76
77    Ok(Self {
78      defaults: Default::default(),
79      actions: Default::default(),
80      file: convert_path_to_absolute(file_name, None)?,
81    })
82  }
83}
84
85impl Context {
86  pub fn subcontext(&self, file: &Path) -> Result<Context, DotfilesError> {
87    Ok(Context {
88      defaults: self.defaults.clone(),
89      actions: Default::default(),
90      file: convert_path_to_absolute(file, self.file.parent())?,
91    })
92  }
93  pub fn get_default(&self, dir: &str, setting: &str) -> Option<&Setting> {
94    self
95      .defaults
96      .get(dir)
97      .and_then(|settings| settings.get(setting))
98  }
99
100  pub fn parse_file(&mut self) -> Result<(), DotfilesError> {
101    {
102      let yaml = read_yaml_file(&self.file)?;
103      if let Some(hash) = yaml.first().and_then(|yaml_first| yaml_first.as_hash()) {
104        if let Some(yaml_defaults) = hash.get(&StrictYaml::String("defaults".into())) {
105          self.defaults = Self::parse_defaults(yaml_defaults)?;
106        }
107        if let Some(yaml_steps) = hash.get(&StrictYaml::String("steps".into())) {
108          let mut local_defaults = self.defaults.clone();
109          self.actions = Self::parse_actions(&mut local_defaults, yaml_steps, self)?;
110        } else {
111          log::warn!(
112            "File {} does not contain any steps to parse",
113            self.file.to_str().unwrap()
114          );
115        }
116
117        fold_hash_until_first_err(
118          yaml.first().unwrap(),
119          Ok(()),
120          |key, _| {
121            if key == "defaults" || key == "steps" {
122              Ok(())
123            } else {
124              Err(DotfilesError::from(
125                format!("Found a  {key} section which I don't know how to process"),
126                dotfiles_core::error::ErrorType::InconsistentConfigurationError,
127              ))
128            }
129          },
130          |_, _| Ok(()),
131        )
132      } else {
133        Err(DotfilesError::from_wrong_yaml(
134          "StrictYaml file root is expected to be a hash that contains defaults and steps"
135            .to_owned(),
136          StrictYaml::BadValue,
137          StrictYaml::Hash(Default::default()),
138        ))
139      }
140    }
141    .map_err(|mut err| {
142      err.add_message_prefix(format!("Parsing {}", self.file.to_str().unwrap()));
143      err
144    })
145  }
146
147  /// parses a list of defaults  from the passed StrictYaml configuration.
148  ///
149  /// It may fail for several reasons:
150  ///
151  /// * [ErrorType::InconsistentConfigurationError]
152  ///   - In case the configuration mentions a directive that doesn't exist
153  /// * [ErrorType::YamlParseError]
154  ///   - A directive is mentioned more than once.
155  ///   - Some other Yaml syntax error.
156  /// * [ErrorType::UnexpectedYamlTypeError]:
157  ///   - The StrictYaml passed to this function is not a Hash.
158  ///   - The hash contains keys that are not Strings.
159  ///   - The StrictYaml contains values that are not Hashes of Strings to settings
160  ///   - The StrictYaml type for a particular setting does not match its expected data type.
161  fn parse_defaults(yaml: &StrictYaml) -> Result<HashMap<String, Settings>, DotfilesError> {
162    fold_hash_until_first_err(
163      yaml,
164      Ok(HashMap::default()),
165      |key, yaml_val| Self::parse_directive_defaults_for_yaml(&key, yaml_val),
166      |mut defaults, (dir_name, settings)| {
167        defaults.insert(dir_name, settings);
168        Ok(defaults)
169      },
170    )
171  }
172
173  fn parse_directive_defaults_for_yaml(
174    directive_name: &str,
175    defaults: &StrictYaml,
176  ) -> Result<(String, Settings), DotfilesError> {
177    let directive = KnownDirective::try_from(directive_name)?;
178    directive.parse_context_defaults(defaults)
179  }
180
181  /// parses a list of actions from the passed StrictYaml configuration.
182  ///
183  /// It may fail for several reasons:
184  ///
185  /// * [ErrorType::InconsistentConfigurationError]
186  ///   - In case the configuration mentions a directive that doesn't exist
187  /// * [ErrorType::YamlParseError]
188  ///   - A directive is mentioned more than once.
189  ///   - Some other Yaml syntax error.
190  /// * [ErrorType::UnexpectedYamlTypeError]:
191  ///   - The StrictYaml passed to this function is not a Hash.
192  ///   - The hash contains keys that are not Strings.
193  ///   - The StrictYaml contains values that are not Hashes of Strings to settings
194  ///   - The StrictYaml type for a particular setting does not match its expected data type.
195  fn parse_actions(
196    defaults: &mut HashMap<String, Settings>,
197    steps_yaml: &StrictYaml,
198    context: &Context,
199  ) -> Result<Vec<KnownAction<'static>>, DotfilesError> {
200    let all_actions: Vec<KnownAction> = map_yaml_array(steps_yaml, |step| {
201      fold_hash_until_first_err(
202        step,
203        Ok(Vec::<KnownAction>::new()),
204        |dir_name, steps_yaml: &StrictYaml| {
205          let directive = KnownDirective::try_from(dir_name.as_str())?;
206          let context_settings = defaults.entry(dir_name).or_default();
207
208          KnownDirective::parse_action_list(directive, context_settings, steps_yaml, context)
209        },
210        |mut existing_actions, mut new_actions| {
211          existing_actions.append(&mut new_actions);
212          Ok(existing_actions)
213        },
214      )
215    })?
216    .into_iter()
217    .flatten()
218    .collect();
219    Ok(all_actions)
220  }
221
222  /// Runs the actions in this context and consumes the context.
223  pub fn run_actions(context: Context) -> Result<(), DotfilesError> {
224    process_until_first_err(context.actions.into_iter(), |action| action.execute())
225  }
226}