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}
32
33impl IcpCommandError {
34    /// Return the typed meaning of external ICP CLI output when Canic recognizes it.
35    #[must_use]
36    pub fn diagnostic(&self) -> Option<IcpDiagnostic> {
37        self.external_output().and_then(classify_icp_diagnostic)
38    }
39
40    /// Return raw external ICP CLI output carried by this error.
41    #[must_use]
42    pub fn external_output(&self) -> Option<&str> {
43        match self {
44            Self::Failed { stderr, .. } => Some(stderr),
45            Self::Json { output, .. } => Some(output),
46            Self::Io(_) | Self::MissingCli { .. } | Self::IncompatibleCliVersion { .. } => None,
47        }
48    }
49}
50
51impl fmt::Display for IcpCommandError {
52    // Render ICP CLI command failures with the command line and captured diagnostics.
53    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            Self::Io(err) => write!(formatter, "{err}"),
56            Self::MissingCli { executable } => {
57                write!(
58                    formatter,
59                    "icp-cli executable not found: {executable}\nrequired: icp-cli {ICP_CLI_SUPPORTED_VERSION_RANGE}\n{}",
60                    icp_cli_install_hint(),
61                )
62            }
63            Self::IncompatibleCliVersion { executable, found } => {
64                write!(
65                    formatter,
66                    "unsupported icp-cli version for {executable}\nfound: {found}\nrequired: icp-cli {ICP_CLI_SUPPORTED_VERSION_RANGE}\n{}",
67                    icp_cli_install_hint(),
68                )
69            }
70            Self::Failed { command, stderr } => {
71                write!(formatter, "icp command failed: {command}\n{stderr}")
72            }
73            Self::Json {
74                command,
75                output,
76                source,
77            } => {
78                write!(
79                    formatter,
80                    "could not parse icp json output for {command}: {source}\n{output}"
81                )
82            }
83        }
84    }
85}
86
87fn icp_cli_install_hint() -> String {
88    format!(
89        "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"
90    )
91}
92
93impl Error for IcpCommandError {
94    // Preserve the underlying I/O error as the source when command execution fails locally.
95    fn source(&self) -> Option<&(dyn Error + 'static)> {
96        match self {
97            Self::Io(err) => Some(err),
98            Self::Json { source, .. } => Some(source),
99            Self::Failed { .. } | Self::IncompatibleCliVersion { .. } | Self::MissingCli { .. } => {
100                None
101            }
102        }
103    }
104}
105
106impl From<std::io::Error> for IcpCommandError {
107    // Convert process-spawn failures into the shared ICP CLI command error type.
108    fn from(err: std::io::Error) -> Self {
109        Self::Io(err)
110    }
111}