cargo-espflash 2.0.0-rc.2

Cargo subcommand for flashing Espressif devices over serial
use std::{
    fmt::{Display, Formatter},
    iter::once,
};

use espflash::targets::Chip;
use miette::{Diagnostic, LabeledSpan, SourceCode, SourceOffset};
use thiserror::Error;

#[derive(Debug, Diagnostic, Error)]
#[non_exhaustive]
pub enum Error {
    #[error("Specified bootloader table is not a bin file")]
    #[diagnostic(code(cargo_espflash::invalid_bootloader_path))]
    InvalidBootloaderPath,

    #[error("Specified partition table is not a csv file")]
    #[diagnostic(code(cargo_espflash::invalid_partition_table_path))]
    InvalidPartitionTablePath,

    #[error("The current workspace is invalid, and could not be loaded")]
    #[diagnostic(
        code(cargo_espflash::invalid_workspace),
        help("Ensure that a valid Cargo.toml file is in the executing directory")
    )]
    InvalidWorkspace,

    #[error("Multiple build artifacts found")]
    #[diagnostic(
        code(cargo_espflash::multiple_artifacts),
        help("Please specify which artifact to flash using --bin")
    )]
    MultipleArtifacts,

    #[error("No executable artifact found")]
    #[diagnostic(
        code(cargo_espflash::no_artifact),
        help("If you're trying to run an example you need to specify it using the `--example` argument\n\
              or if you're in a cargo workspace, specify the binary package with `--package`.")
    )]
    NoArtifact,

    #[error("'build-std' not configured")]
    #[diagnostic(
        code(cargo_espflash::no_build_std),
        help(
            "cargo currently requires the unstable 'build-std' feature, ensure \
            that .cargo/config{{.toml}} has the appropriate options.\n  \
            \tSee: https://doc.rust-lang.org/cargo/reference/unstable.html#build-std"
        )
    )]
    NoBuildStd,

    #[error("No package could be located in the current workspace")]
    #[diagnostic(
        code(cargo_espflash::no_package),
        help("Ensure that you are executing from a valid package, or that the specified package name\
              exists in the current workspace.")
    )]
    NoPackage,

    #[error("No Cargo.toml found in the current directory")]
    #[diagnostic(
        code(cargo_espflash::no_project),
        help("Ensure that you're running the command from within a cargo project")
    )]
    NoProject,

    #[error(transparent)]
    #[diagnostic(transparent)]
    NoTarget(#[from] NoTargetError),

    #[error(transparent)]
    #[diagnostic(transparent)]
    UnsupportedTarget(UnsupportedTargetError),
}

#[derive(Debug)]
pub struct TomlError {
    err: toml::de::Error,
    source: String,
}

impl TomlError {
    pub fn new(err: toml::de::Error, source: String) -> Self {
        Self { err, source }
    }
}

impl Display for TomlError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "Failed to parse toml")
    }
}

// NOTE: no `source` on purpose to prevent duplicating the message
impl std::error::Error for TomlError {}

impl Diagnostic for TomlError {
    fn source_code(&self) -> Option<&dyn SourceCode> {
        Some(&self.source)
    }

    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
        let (line, col) = self.err.line_col()?;
        let offset = SourceOffset::from_location(&self.source, line + 1, col + 1);

        Some(Box::new(once(LabeledSpan::new(
            Some(self.err.to_string()),
            offset.offset(),
            0,
        ))))
    }
}

#[derive(Debug, Diagnostic, Error)]
#[error("Target {target} is not supported by the {chip}")]
#[diagnostic(
    code(cargo_espflash::unsupported_target),
    help("The following targets are supported by the {}: {}", self.chip, self.supported_targets())
)]
pub struct UnsupportedTargetError {
    target: String,
    chip: Chip,
}

impl UnsupportedTargetError {
    pub fn new(target: &str, chip: Chip) -> Self {
        Self {
            target: target.into(),
            chip,
        }
    }
}

impl UnsupportedTargetError {
    fn supported_targets(&self) -> String {
        self.chip.into_target().supported_build_targets().join(", ")
    }
}

#[derive(Debug, Error)]
#[error("No target specified in cargo configuration")]
pub struct NoTargetError {
    chip: Option<Chip>,
}

impl NoTargetError {
    pub fn new(chip: Option<Chip>) -> Self {
        Self { chip }
    }
}

impl Diagnostic for NoTargetError {
    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        Some(Box::new("cargo_espflash::no_target"))
    }

    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        Some(Box::new(match &self.chip {
            Some(chip) => format!(
                "Specify the target in `.cargo/config.toml`, the {} support the following targets: {}",
                chip,
                chip.into_target().supported_build_targets().join(", ")
            ),
            None => "Specify the target in `.cargo/config.toml`".into(),
        }
        ))
    }
}