Skip to main content

dotfiles_core/
error.rs

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