Skip to main content

flake_edit/app/
error.rs

1use std::path::PathBuf;
2
3use crate::change::ChangeId;
4use crate::config::ConfigError;
5use crate::follows::path::AttrPathParseError;
6use crate::validate::ValidationError;
7
8/// Errors raised inside the binary's command and handler layer.
9///
10/// One layer combining the per-subcommand operations and the top-level
11/// dispatcher.
12#[derive(Debug, thiserror::Error)]
13#[non_exhaustive]
14pub enum Error {
15    /// A failure inside the library edit / walk / lock layer.
16    #[error(transparent)]
17    Flake(#[from] crate::Error),
18
19    /// A configuration loading failure.
20    #[error(transparent)]
21    Config(#[from] ConfigError),
22
23    /// An io error not otherwise classified (e.g. nix subprocess failure).
24    #[error("io error: {0}")]
25    Io(#[from] std::io::Error),
26
27    /// `flake.nix` could not be opened or located.
28    #[error("could not open flake.nix at {path}", path = path.display())]
29    FlakeNotFound {
30        path: PathBuf,
31        #[source]
32        source: std::io::Error,
33    },
34
35    /// The directory passed to `--flake` exists but contains no `flake.nix`.
36    #[error("no flake.nix in directory {path}", path = path.display())]
37    FlakeDirEmpty { path: PathBuf },
38
39    /// `--flake` and `--lock` were combined with the batch
40    /// `follow [PATHS...]` form, which owns its own per-file editor.
41    #[error("`--flake` and `--lock` cannot be used with `follow [PATHS]`")]
42    IncompatibleFollowOptions,
43
44    /// A subcommand was invoked without a URI argument when one is required.
45    #[error("no URI provided")]
46    NoUri,
47
48    /// A subcommand was invoked without an input id when one is required.
49    #[error("no input id provided")]
50    NoId,
51
52    /// An input list was empty when at least one was required.
53    #[error("no inputs found in the flake")]
54    NoInputs,
55
56    /// A flake reference could not be parsed by `nix_uri`.
57    #[error("invalid URI '{uri}'")]
58    InvalidUri {
59        uri: String,
60        #[source]
61        source: nix_uri::NixUriError,
62    },
63
64    /// An input id was malformed; carries the typed parse error.
65    #[error("invalid input id '{id}'")]
66    InvalidInputId {
67        id: String,
68        #[source]
69        source: AttrPathParseError,
70    },
71
72    /// A follows path was malformed; carries the typed parse error.
73    #[error("invalid follows path '{path}'")]
74    InvalidFollowsPath {
75        path: String,
76        #[source]
77        source: AttrPathParseError,
78    },
79
80    /// `nix_uri` rendered a flake reference but could not infer an id from it.
81    #[error("could not infer id from flake reference '{uri}'")]
82    CouldNotInferId { uri: String },
83
84    /// The named input has no concrete URL to pin against (e.g. a
85    /// `follows`-only input or a non-standard reference shape).
86    #[error("input '{id}' has no pinnable URL (it may use follows or a non-standard format)")]
87    InputNotPinnable { id: String },
88
89    /// Removing an input did not produce a syntax change.
90    #[error("could not remove input '{id}'")]
91    CouldNotRemove { id: ChangeId },
92
93    /// Could not load `flake.lock`. The wrapped library error already
94    /// classifies the underlying failure.
95    #[error("could not read lock file '{path}'", path = path.display())]
96    LockFile {
97        path: PathBuf,
98        #[source]
99        source: crate::Error,
100    },
101
102    /// A `follow <input> <target>` invocation could not establish the
103    /// follows relationship.
104    #[error("could not create follows relationship for '{id}'")]
105    FollowsCreateFailed { id: String },
106
107    /// Validation of `flake.nix` failed after applying speculative edits.
108    /// Distinct from `crate::Error::Validation` (which fires before edits)
109    /// because the diagnostic flow needs to render the staged edits too.
110    #[error("validation failed after applying edits ({} issue(s))", .0.len())]
111    ValidationAfterEdit(Vec<ValidationError>),
112
113    /// Aggregated failures from a `follow [PATHS...]` batch. Each entry
114    /// pairs the offending path with the error processing it produced.
115    #[error("{} file(s) failed during batch processing", failures.len())]
116    Batch {
117        failures: Vec<(PathBuf, Box<Error>)>,
118    },
119
120    /// `toggle` was invoked without arguments and no input stores an
121    /// alternate.
122    #[error("no toggleable inputs in flake.nix")]
123    NoToggleableInputs,
124
125    /// `toggle` was invoked without arguments, several inputs store
126    /// alternates, and no interactive picker is available.
127    #[error("multiple toggleable inputs")]
128    MultipleToggleableInputs { candidates: Vec<ToggleCandidate> },
129
130    /// `toggle` named an input id that the flake does not declare.
131    #[error("no input named '{id}' in flake.nix")]
132    ToggleUnknownInput { id: String },
133
134    /// `toggle` named an input that has no stored alternate to flip to.
135    #[error("input '{id}' has no stored alternate")]
136    ToggleNoAlternate { id: String },
137
138    /// The input stores several alternates and no interactive picker is
139    /// available to choose between them.
140    #[error(
141        "input '{id}' has {count} variants, cannot infer which to {action}",
142        count = alternates.len() + 1
143    )]
144    ToggleAmbiguousVariant {
145        id: String,
146        active: String,
147        alternates: Vec<String>,
148        action: ToggleAction,
149    },
150
151    /// The matching ladder found no input for the given reference.
152    #[error("could not match '{reference}' to any input")]
153    ToggleRefUnmatched { reference: String },
154
155    /// One matching-ladder stage yielded several inputs and no interactive
156    /// picker is available.
157    #[error("'{reference}' matches multiple inputs")]
158    ToggleRefAmbiguous {
159        reference: String,
160        candidates: Vec<RefCandidate>,
161    },
162
163    /// A path-shaped reference does not exist on disk.
164    #[error("could not read path '{path}'")]
165    TogglePathMissing {
166        path: String,
167        #[source]
168        source: std::io::Error,
169    },
170
171    /// The named reference is already the input's active variant and no
172    /// alternate is stored to flip back to.
173    #[error(
174        "'{reference}' is already the active variant of '{id}'\n       and no alternate is stored"
175    )]
176    ToggleAlreadyActive { reference: String, id: String },
177
178    /// `toggle --remove` resolved a reference that is not stored on the
179    /// input, so there is nothing to remove.
180    #[error("'{reference}' is not a stored variant of '{id}'")]
181    ToggleRemoveUnstored {
182        reference: String,
183        id: String,
184        active: String,
185        alternates: Vec<String>,
186    },
187
188    /// `toggle --remove` named the input's active url while no alternate
189    /// is stored to take its place. Honoring it would leave the input
190    /// url-less.
191    #[error("'{reference}' is the active url of '{id}' and no alternate is stored to replace it")]
192    ToggleRemoveActive { reference: String, id: String },
193}
194
195/// What `toggle` does with the resolved variant, for error wording.
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum ToggleAction {
198    /// Make the variant the active url.
199    Activate,
200    /// Delete the variant's line.
201    Remove,
202}
203
204impl std::fmt::Display for ToggleAction {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        match self {
207            Self::Activate => write!(f, "activate"),
208            Self::Remove => write!(f, "remove"),
209        }
210    }
211}
212
213/// One toggleable input named in [`Error::MultipleToggleableInputs`].
214#[derive(Debug, Clone)]
215pub struct ToggleCandidate {
216    /// Input id.
217    pub id: String,
218    /// Every variant of the input, the active one first.
219    pub variants: Vec<String>,
220}
221
222/// One input the matching ladder offered for an ambiguous ref, named in
223/// [`Error::ToggleRefAmbiguous`].
224#[derive(Debug, Clone)]
225pub struct RefCandidate {
226    /// Input id.
227    pub id: String,
228    /// The variant that matched and the way it matched.
229    pub reason: String,
230}
231
232impl Error {
233    /// Per-failure rendering of a `Batch` aggregate. Each item joins the
234    /// path with the error and its full source chain so a reader sees the
235    /// underlying cause without the renderer descending per-bullet.
236    pub fn batch_bullets(&self) -> Option<Vec<String>> {
237        match self {
238            Self::Batch { failures } => Some(
239                failures
240                    .iter()
241                    .map(|(path, err)| {
242                        format!(
243                            "{}: {}",
244                            path.display(),
245                            chain_layers(err.as_ref()).join(": ")
246                        )
247                    })
248                    .collect(),
249            ),
250            _ => None,
251        }
252    }
253
254    /// Per-error rendering of a `ValidationAfterEdit` aggregate. Returns
255    /// `None` for non-aggregate variants.
256    pub fn validation_bullets(&self) -> Option<Vec<String>> {
257        match self {
258            Self::ValidationAfterEdit(errs) => Some(errs.iter().map(|e| e.to_string()).collect()),
259            _ => None,
260        }
261    }
262
263    /// Candidate listings for the ambiguous `toggle` errors, one bullet per
264    /// candidate. Returns `None` for other variants.
265    pub fn candidate_bullets(&self) -> Option<Vec<String>> {
266        match self {
267            Self::MultipleToggleableInputs { candidates } => Some(
268                candidates
269                    .iter()
270                    .map(|c| format!("{}: {}", c.id, c.variants.join(" <-> ")))
271                    .collect(),
272            ),
273            Self::ToggleAmbiguousVariant {
274                active, alternates, ..
275            }
276            | Self::ToggleRemoveUnstored {
277                active, alternates, ..
278            } => Some(
279                std::iter::once(format!("{active} (active)"))
280                    .chain(alternates.iter().cloned())
281                    .collect(),
282            ),
283            Self::ToggleRefAmbiguous { candidates, .. } => Some(
284                candidates
285                    .iter()
286                    .map(|c| format!("{} ({})", c.id, c.reason))
287                    .collect(),
288            ),
289            _ => None,
290        }
291    }
292}
293
294/// Walk an error's source chain top-down, returning the `Display` of
295/// each layer.
296///
297/// No dedup: every variant is either `#[error(transparent)]` or wraps
298/// its source with distinct outer text, so adjacent layers never repeat.
299/// A new `#[error("{0}")]` with `#[from]` would break this; fix at the
300/// variant, not here.
301pub fn chain_layers(err: &(dyn std::error::Error + 'static)) -> Vec<String> {
302    let mut layers = vec![err.to_string()];
303    let mut current = err.source();
304    while let Some(source) = current {
305        layers.push(source.to_string());
306        current = source.source();
307    }
308    layers
309}
310
311/// Local result type for app-layer code.
312pub type Result<T> = std::result::Result<T, Error>;