flake_edit/error.rs
1use std::path::PathBuf;
2
3use crate::lock::LockError;
4use crate::validate::ValidationError;
5use crate::walk::WalkerError;
6
7/// Error for [`crate::edit::FlakeEdit`] operations.
8#[derive(Debug, thiserror::Error)]
9#[non_exhaustive]
10pub enum Error {
11 /// Failed to read a flake file. Carries the path that the read was
12 /// attempted against so the caller can surface it.
13 #[error("failed to read {path}", path = path.display())]
14 Read {
15 path: PathBuf,
16 #[source]
17 source: std::io::Error,
18 },
19 /// Failed to write a flake file.
20 #[error("failed to write {path}", path = path.display())]
21 Write {
22 path: PathBuf,
23 #[source]
24 source: std::io::Error,
25 },
26 /// The CST walker rejected a change. See [`WalkerError`] for details.
27 #[error(transparent)]
28 Walker(#[from] WalkerError),
29 /// A failure parsing or walking `flake.lock`. See [`LockError`] for the
30 /// per-variant breakdown.
31 #[error(transparent)]
32 Lock(#[from] LockError),
33 /// Tried to add an input that already exists. The wrapped string is the
34 /// existing input id.
35 #[error("input '{0}' already exists in the flake")]
36 DuplicateInput(String),
37 /// Tried to operate on an input id that is not declared in the flake.
38 #[error("input '{0}' not found in the flake")]
39 InputNotFound(String),
40 /// Tried to toggle an input that has no url binding (e.g. a
41 /// follows-only input).
42 #[error("input '{0}' has no url to toggle (follows-only input)")]
43 NoUrlToToggle(String),
44 /// Tried to remove an input's active url without a stored alternate to
45 /// take its place. Honoring it would leave the input url-less.
46 #[error("cannot remove the active url of '{0}' without an alternate to activate")]
47 RemoveActiveWithoutAlternate(String),
48 /// The `add-follow` subcommand received a path deeper than `parent.child`.
49 /// `flake-edit follow` accepts deeper paths, bounded by
50 /// [`crate::config::FollowConfig::max_depth`] when that is set; this
51 /// guard catches typos in the explicit-path command before they produce
52 /// nested `inputs.*.inputs.*.follows` chains.
53 #[error(
54 "`add-follow` accepts only depth-1 paths of the form `parent.child`; got '{path}' ({segments} segments)"
55 )]
56 AddFollowDepthLimit { path: String, segments: usize },
57 /// Pre-edit validation found one or more fatal issues in `flake.nix`.
58 #[error("validation failed in flake.nix ({} issue(s))", .0.len())]
59 Validation(Vec<ValidationError>),
60}
61
62impl Error {
63 /// Actionable hint to display alongside the error, when one exists.
64 ///
65 /// Hints live here rather than in `#[error(...)]` strings so the binary
66 /// can render them on a separate `hint:` line and library callers can
67 /// choose to surface or ignore them.
68 pub fn hint(&self) -> Option<String> {
69 match self {
70 Self::DuplicateInput(id) => Some(format!(
71 "to replace it, run `flake-edit remove {id}` then `flake-edit add {id} <flakeref>`; \
72 or add it under a different id with `flake-edit add [ID] <flakeref>`"
73 )),
74 Self::InputNotFound(id) => Some(format!(
75 "to add it, run `flake-edit add {id} <flakeref>`; \
76 see declared inputs with `flake-edit list`"
77 )),
78 Self::AddFollowDepthLimit { .. } => Some(
79 "use `flake-edit follow` for deeper paths (depth bounded by `follow.max_depth` in your config, if set)"
80 .into(),
81 ),
82 _ => None,
83 }
84 }
85
86 /// Per-error rendering of a `Validation` aggregate as one bullet per
87 /// inner error. Returns `None` for non-aggregate variants.
88 pub fn bullets(&self) -> Option<Vec<String>> {
89 match self {
90 Self::Validation(errors) => Some(errors.iter().map(|e| e.to_string()).collect()),
91 _ => None,
92 }
93 }
94}