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>;