Skip to main content

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}