1use 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#[derive(Getters)]
45pub struct Context {
46 #[getset(get = "pub")]
48 defaults: HashMap<String, Settings>,
49 #[getset(get = "pub")]
51 actions: Vec<KnownAction<'static>>,
52 #[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 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 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 pub fn run_actions(context: Context) -> Result<(), DotfilesError> {
224 process_until_first_err(context.actions.into_iter(), |action| action.execute())
225 }
226}