dotfiles_core/
error.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
22//! Module for the error handling classes and enums.
23
24use std::fmt::Formatter;
25
26use getset::Getters;
27use itertools::fold;
28use std::fmt::Display;
29use strict_yaml_rust::ScanError;
30use strict_yaml_rust::StrictYaml;
31use subprocess::ExitStatus;
32use subprocess::PopenError;
33
34use crate::Directive;
35
36/// Executes the `process_function` on each of the items in the `iterable`, and then returns
37/// `Ok(())`. It stops execution if any of the process functions returns an Error, and returns said
38/// error.
39pub fn process_until_first_err<I, F, E>(iterable: I, mut process_function: F) -> Result<(), E>
40where
41  I: IntoIterator,
42  F: FnMut(I::Item) -> Result<(), E>,
43{
44  fold(iterable, Ok(()), |prev_res, item| match prev_res {
45    Ok(()) => process_function(item),
46    Err(err) => Err(err),
47  })
48}
49
50/// Executes the `process_function` on each of the items in the `iterable`, and folds them using the
51/// `fold_function`. Returns the processed and folded items if all the processing and folding was
52/// successful, otherwise returns the first error found.
53pub fn fold_until_first_err<I, Folded, Processed, F, P, E>(
54  iterable: I,
55  init: Result<Folded, E>,
56  process_function: P,
57  mut fold_function: F,
58) -> Result<Folded, E>
59where
60  I: IntoIterator,
61  F: FnMut(Folded, Processed) -> Result<Folded, E>,
62  P: FnMut(I::Item) -> Result<Processed, E>,
63{
64  let processed_vec_res: Result<Vec<Processed>, E> =
65    iterable.into_iter().map(process_function).collect();
66
67  processed_vec_res.and_then(|processed_vec| {
68    fold(
69      processed_vec.into_iter(),
70      init,
71      |prev_res, item| match prev_res {
72        Ok(prev_folded) => fold_function(prev_folded, item),
73        Err(err) => Err(err),
74      },
75    )
76  })
77}
78
79/// A collection of types of errors that may occur while parsing or executing actions
80#[derive(Debug)]
81pub enum ErrorType {
82  /// An error occurred while running a command necessary for executing an action
83  ExecutionError {
84    /// If the command could not execute for some reason the underlying Popen Error will be saved
85    /// here
86    popen_error: Option<PopenError>,
87    /// If the command attempted to execute but failed for some reason, the underlying ExitStatus
88    /// will be saved here.
89    exit_status: Option<ExitStatus>,
90  },
91  /// A filesystem error that was encountered while either reading configuration or
92  /// executing a filesystem related action
93  FileSystemError {
94    /// The underlying filesystem error.
95    fs_error: std::io::Error,
96  },
97  /// The configuration file is inconsistent with itself or with that dotfiles supports.
98  InconsistentConfigurationError,
99  /// The configuration is missing a required field
100  IncompleteConfigurationError {
101    /// Name of the field missing in the configuration
102    missing_field: String,
103  },
104  /// An error that occurred while parsing the StrictYaml file
105  YamlParseError {
106    /// The underlying scan error
107    scan_error: ScanError,
108  },
109  /// Received an StrictYaml object of an unexpected type
110  UnexpectedYamlTypeError {
111    /// What we got instead of the expected type.
112    encountered: StrictYaml,
113    /// An example of what we expected.
114    expected: StrictYaml,
115  },
116  /// A core logic error for Dotfiles-rs
117  CoreError,
118  /// An error only for testing, the action that should fail actually succeeds!
119  TestingErrorActionSucceedsWhenItShouldFail,
120}
121
122impl Display for ErrorType {
123  fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
124    write!(
125      f,
126      "{}",
127      match self {
128        ErrorType::ExecutionError {
129          popen_error: _,
130          exit_status: _,
131        } => "ExecutionError",
132        ErrorType::FileSystemError { fs_error: _ } => "FileSystemError",
133        ErrorType::InconsistentConfigurationError => "InconsistentConfigurationError",
134        ErrorType::IncompleteConfigurationError { missing_field: _ } =>
135          "IncompleteConfigurationError",
136        ErrorType::YamlParseError { scan_error: _ } => "YamlParseError",
137        ErrorType::UnexpectedYamlTypeError {
138          encountered: _,
139          expected: _,
140        } => "UnexpectedYamlTypeError",
141        ErrorType::CoreError => "CoreError",
142        ErrorType::TestingErrorActionSucceedsWhenItShouldFail =>
143          "TestingErrorActionSucceedsWhenItShouldFail",
144      }
145    )
146  }
147}
148
149/// Creates an [ErrorType::ExecutionError]
150pub fn execution_error(
151  popen_error: Option<PopenError>,
152  exit_status: Option<ExitStatus>,
153) -> ErrorType {
154  ErrorType::ExecutionError {
155    popen_error,
156    exit_status,
157  }
158}
159
160/// Struct that represents an error that happened while parsing or executing actions.
161#[derive(Getters, Debug)]
162pub struct DotfilesError {
163  /// Human-readable error message
164  #[getset(get = "pub")]
165  message: String,
166  /// [Error type](ErrorType)
167  #[getset(get = "pub")]
168  error_type: ErrorType,
169}
170
171impl Display for DotfilesError {
172  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
173    write!(f, "{}: {}", self.error_type(), self.message())
174  }
175}
176
177impl DotfilesError {
178  /// Adds a prefix to the existing message
179  pub fn add_message_prefix(&mut self, prefix: String) {
180    self.message = format!("{prefix}: {}", self.message,);
181  }
182  /// returns whether the underlying error is a missing configuration
183  pub fn is_missing_config(&self, config_name: &str) -> bool {
184    match &self.error_type {
185      ErrorType::IncompleteConfigurationError { missing_field } => missing_field == config_name,
186      _ => false,
187    }
188  }
189
190  /// Returns whether the error is a wrong yaml type.
191  pub fn is_wrong_yaml(&self) -> bool {
192    matches!(
193      &self.error_type,
194      ErrorType::UnexpectedYamlTypeError {
195        encountered: _,
196        expected: _,
197      }
198    )
199  }
200
201  /// Returns whether the error is a wrong yaml type.
202  pub fn is_yaml_parse_error(&self) -> bool {
203    matches!(
204      &self.error_type,
205      ErrorType::YamlParseError { scan_error: _ }
206    )
207  }
208
209  /// Returns whether the error is an Inconsistent Config.
210  pub fn is_inconsistent_config(&self) -> bool {
211    matches!(&self.error_type, ErrorType::InconsistentConfigurationError)
212  }
213  /// Returns whether the error is a Fs error.
214  pub fn is_fs_error(&self) -> bool {
215    matches!(&self.error_type, ErrorType::FileSystemError { fs_error: _ })
216  }
217
218  /// Creates a new Dotfiles error with the given message and error type
219  pub fn from(message: String, error_type: ErrorType) -> Self {
220    DotfilesError {
221      message,
222      error_type,
223    }
224  }
225
226  /// Creates a new Dotfiles error with the given message and error type
227  pub fn from_wrong_yaml(
228    message: String,
229    wrong_yaml: StrictYaml,
230    expected_type: StrictYaml,
231  ) -> Self {
232    DotfilesError {
233      message,
234      error_type: ErrorType::UnexpectedYamlTypeError {
235        encountered: wrong_yaml,
236        expected: expected_type,
237      },
238    }
239  }
240  /// Creates a new Dotfiles error with the given message and error type
241  pub fn from_io_error(io_error: std::io::Error) -> Self {
242    DotfilesError {
243      message: io_error.to_string(),
244      error_type: ErrorType::FileSystemError { fs_error: io_error },
245    }
246  }
247}
248
249/// Adds a prefix to an error with the name of the directive where it happened
250pub fn add_directive_error_prefix<'a, D, T>(
251  dir: &'a D,
252  res: Result<T, DotfilesError>,
253) -> Result<T, DotfilesError>
254where
255  D: Directive<'a>,
256{
257  res.map_err(|mut error| {
258    error.add_message_prefix(format!("Directive {}", dir.name()));
259    error
260  })
261}