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
121impl Error {
122    /// Per-failure rendering of a `Batch` aggregate. Each item joins the
123    /// path with the error and its full source chain so a reader sees the
124    /// underlying cause without the renderer descending per-bullet.
125    pub fn batch_bullets(&self) -> Option<Vec<String>> {
126        match self {
127            Self::Batch { failures } => Some(
128                failures
129                    .iter()
130                    .map(|(path, err)| {
131                        format!(
132                            "{}: {}",
133                            path.display(),
134                            chain_layers(err.as_ref()).join(": ")
135                        )
136                    })
137                    .collect(),
138            ),
139            _ => None,
140        }
141    }
142
143    /// Per-error rendering of a `ValidationAfterEdit` aggregate. Returns
144    /// `None` for non-aggregate variants.
145    pub fn validation_bullets(&self) -> Option<Vec<String>> {
146        match self {
147            Self::ValidationAfterEdit(errs) => Some(errs.iter().map(|e| e.to_string()).collect()),
148            _ => None,
149        }
150    }
151}
152
153/// Walk an error's source chain top-down, returning the `Display` of
154/// each layer.
155///
156/// No dedup: every variant is either `#[error(transparent)]` or wraps
157/// its source with distinct outer text, so adjacent layers never repeat.
158/// A new `#[error("{0}")]` with `#[from]` would break this; fix at the
159/// variant, not here.
160pub fn chain_layers(err: &(dyn std::error::Error + 'static)) -> Vec<String> {
161    let mut layers = vec![err.to_string()];
162    let mut current = err.source();
163    while let Some(source) = current {
164        layers.push(source.to_string());
165        current = source.source();
166    }
167    layers
168}
169
170/// Local result type for app-layer code.
171pub type Result<T> = std::result::Result<T, Error>;