biors 0.37.2

Command-line tools for bio-rs biological AI model input workflows.
use crate::exit_code;
use biors_core::{
    error::{BioRsError, ErrorLocation, FastaReadError},
    model_input::ModelInputBuildError,
    package::{PackageValidationIssueCode, PackageValidationReport},
    verification::{PackageVerificationReport, VerificationIssueCode, VerificationStatus},
};
use serde::Serialize;
use std::path::PathBuf;

#[derive(Debug)]
pub(crate) enum CliError {
    Core(BioRsError),
    ModelInput(ModelInputBuildError),
    Json(serde_json::Error),
    CurrentDir(std::io::Error),
    Read {
        path: PathBuf,
        source: std::io::Error,
    },
    Serialization(serde_json::Error),
    Write(std::io::Error),
    Validation {
        code: &'static str,
        message: String,
        location: Option<String>,
    },
}

#[derive(Debug, Serialize)]
#[serde(untagged)]
pub(crate) enum ErrorLocationValue {
    Core(ErrorLocation),
    Label(String),
}

impl CliError {
    pub(crate) const fn code(&self) -> &'static str {
        match self {
            Self::Core(error) => error.code(),
            Self::ModelInput(ModelInputBuildError::InvalidPolicy { .. }) => {
                "model_input.invalid_policy"
            }
            Self::ModelInput(ModelInputBuildError::InvalidTokenizedSequence { .. }) => {
                "model_input.invalid_sequence"
            }
            Self::Json(_) => "json.invalid",
            Self::CurrentDir(_) => "io.read_failed",
            Self::Read { .. } => "io.read_failed",
            Self::Serialization(_) => "json.serialization_failed",
            Self::Write(_) => "io.write_failed",
            Self::Validation { code, .. } => code,
        }
    }

    pub(crate) fn location(&self) -> Option<ErrorLocationValue> {
        match self {
            Self::Core(error) => error.location().map(ErrorLocationValue::Core),
            Self::ModelInput(ModelInputBuildError::InvalidPolicy { .. }) => None,
            Self::ModelInput(ModelInputBuildError::InvalidTokenizedSequence { id, .. }) => {
                Some(ErrorLocationValue::Label(id.clone()))
            }
            Self::Read { path, .. } => Some(ErrorLocationValue::Label(path.display().to_string())),
            Self::Validation { location, .. } => location.clone().map(ErrorLocationValue::Label),
            Self::Json(_) | Self::CurrentDir(_) | Self::Serialization(_) | Self::Write(_) => None,
        }
    }

    pub(crate) const fn exit_code(&self) -> i32 {
        match self {
            Self::Core(_) | Self::ModelInput(_) | Self::Json(_) | Self::Validation { .. } => {
                exit_code::USER_INPUT_FAILURE
            }
            Self::Read { .. } | Self::CurrentDir(_) | Self::Serialization(_) | Self::Write(_) => {
                exit_code::IO_OR_INTERNAL_FAILURE
            }
        }
    }

    pub(crate) fn from_fasta_read(path: PathBuf, error: FastaReadError) -> Self {
        match error {
            FastaReadError::Parse(error) => Self::Core(error),
            FastaReadError::Io(source) => Self::Read { path, source },
        }
    }
}

impl std::fmt::Display for CliError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Core(error) => write!(f, "{error}"),
            Self::ModelInput(error) => write!(f, "{error}"),
            Self::Json(error) => write!(f, "{error}"),
            Self::CurrentDir(error) => write!(f, "failed to determine current directory: {error}"),
            Self::Read { path, source } => {
                write!(f, "failed to read '{}': {source}", path.display())
            }
            Self::Serialization(error) => write!(f, "{error}"),
            Self::Write(error) => write!(f, "failed to write output: {error}"),
            Self::Validation { message, .. } => write!(f, "{message}"),
        }
    }
}

impl std::error::Error for CliError {}

impl From<BioRsError> for CliError {
    fn from(error: BioRsError) -> Self {
        Self::Core(error)
    }
}

impl From<ModelInputBuildError> for CliError {
    fn from(error: ModelInputBuildError) -> Self {
        Self::ModelInput(error)
    }
}

impl From<serde_json::Error> for CliError {
    fn from(error: serde_json::Error) -> Self {
        Self::Serialization(error)
    }
}

pub(crate) fn classify_validation_code(report: &PackageValidationReport) -> &'static str {
    if report
        .structured_issues
        .iter()
        .any(|issue| issue.code == PackageValidationIssueCode::InvalidChecksumFormat)
    {
        "package.invalid_checksum_format"
    } else if report
        .structured_issues
        .iter()
        .any(|issue| issue.code == PackageValidationIssueCode::ChecksumMismatch)
    {
        "package.checksum_mismatch"
    } else if report
        .structured_issues
        .iter()
        .any(|issue| issue.code == PackageValidationIssueCode::InvalidAssetPath)
    {
        "package.invalid_asset_path"
    } else if report
        .structured_issues
        .iter()
        .any(|issue| issue.code == PackageValidationIssueCode::LayoutMismatch)
    {
        "package.layout_mismatch"
    } else if report
        .structured_issues
        .iter()
        .any(|issue| issue.code == PackageValidationIssueCode::AssetReadFailed)
    {
        "package.asset_read_failed"
    } else {
        "package.validation_failed"
    }
}

pub(crate) fn classify_verification_code(report: &PackageVerificationReport) -> &'static str {
    if report
        .results
        .iter()
        .any(|result| result.issue_code == Some(VerificationIssueCode::ObservationMissing))
    {
        "package.observed_output_missing"
    } else if report
        .results
        .iter()
        .any(|result| result.issue_code == Some(VerificationIssueCode::ObservationPathInvalid))
    {
        "package.invalid_asset_path"
    } else if report.results.iter().any(|result| {
        result.issue_code == Some(VerificationIssueCode::ObservedOutputReadFailed)
            || matches!(result.status, VerificationStatus::Missing)
    }) {
        "package.observed_output_missing"
    } else if report.results.iter().any(|result| result.content_mismatch) {
        "package.output_content_mismatch"
    } else if report.results.iter().any(|result| result.checksum_mismatch) {
        "package.checksum_mismatch"
    } else {
        "package.verification_failed"
    }
}