Skip to main content

canic_host/icp/
error.rs

1use std::{error::Error, fmt};
2
3use super::{
4    diagnostic::{IcpDiagnostic, classify_icp_diagnostic},
5    model::{ICP_CLI_SUPPORTED_VERSION_RANGE, REQUIRED_ICP_CLI_VERSION},
6};
7
8///
9/// IcpCommandError
10///
11
12#[derive(Debug)]
13pub enum IcpCommandError {
14    Io(std::io::Error),
15    MissingCli {
16        executable: String,
17    },
18    IncompatibleCliVersion {
19        executable: String,
20        found: String,
21    },
22    Failed {
23        command: String,
24        stderr: String,
25    },
26    Json {
27        command: String,
28        output: String,
29        source: serde_json::Error,
30    },
31    SnapshotIdUnavailable {
32        output: String,
33    },
34}
35
36impl IcpCommandError {
37    /// Return the typed meaning of external ICP CLI output when Canic recognizes it.
38    #[must_use]
39    pub fn diagnostic(&self) -> Option<IcpDiagnostic> {
40        self.external_output().and_then(classify_icp_diagnostic)
41    }
42
43    /// Return raw external ICP CLI output carried by this error.
44    #[must_use]
45    pub fn external_output(&self) -> Option<&str> {
46        match self {
47            Self::Failed { stderr, .. } => Some(stderr),
48            Self::Json { output, .. } | Self::SnapshotIdUnavailable { output } => Some(output),
49            Self::Io(_) | Self::MissingCli { .. } | Self::IncompatibleCliVersion { .. } => None,
50        }
51    }
52}
53
54impl fmt::Display for IcpCommandError {
55    // Render ICP CLI command failures with the command line and captured diagnostics.
56    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match self {
58            Self::Io(err) => write!(formatter, "{err}"),
59            Self::MissingCli { executable } => {
60                write!(
61                    formatter,
62                    "icp-cli executable not found: {executable}\nrequired: icp-cli {ICP_CLI_SUPPORTED_VERSION_RANGE}\n{}",
63                    icp_cli_install_hint(),
64                )
65            }
66            Self::IncompatibleCliVersion { executable, found } => {
67                write!(
68                    formatter,
69                    "unsupported icp-cli version for {executable}\nfound: {found}\nrequired: icp-cli {ICP_CLI_SUPPORTED_VERSION_RANGE}\n{}",
70                    icp_cli_install_hint(),
71                )
72            }
73            Self::Failed { command, stderr } => {
74                write!(formatter, "icp command failed: {command}\n{stderr}")
75            }
76            Self::Json {
77                command,
78                output,
79                source,
80            } => {
81                write!(
82                    formatter,
83                    "could not parse icp json output for {command}: {source}\n{output}"
84                )
85            }
86            Self::SnapshotIdUnavailable { output } => {
87                write!(
88                    formatter,
89                    "could not parse snapshot id from icp output: {output}"
90                )
91            }
92        }
93    }
94}
95
96fn icp_cli_install_hint() -> String {
97    format!(
98        "next: install checksum-verified icp-cli {REQUIRED_ICP_CLI_VERSION} or newer and ensure it is first on PATH\n  from a Canic checkout: make install-dev\nnote: `icp network update` updates the local network launcher, not the `icp` CLI binary; pass top-level --icp <path> to use a non-PATH install"
99    )
100}
101
102impl Error for IcpCommandError {
103    // Preserve the underlying I/O error as the source when command execution fails locally.
104    fn source(&self) -> Option<&(dyn Error + 'static)> {
105        match self {
106            Self::Io(err) => Some(err),
107            Self::Json { source, .. } => Some(source),
108            Self::Failed { .. }
109            | Self::IncompatibleCliVersion { .. }
110            | Self::MissingCli { .. }
111            | Self::SnapshotIdUnavailable { .. } => None,
112        }
113    }
114}
115
116impl From<std::io::Error> for IcpCommandError {
117    // Convert process-spawn failures into the shared ICP CLI command error type.
118    fn from(err: std::io::Error) -> Self {
119        Self::Io(err)
120    }
121}