biors 0.58.0

Command-line tools for bio-rs biological AI model input workflows.
mod classification;

use crate::exit_code;
use biors_core::{
    error::{BioRsError, Diagnostic, ErrorLocation, FastaReadError},
    formats::FormatReadError,
    model_input::ModelInputBuildError,
    molecule::MoleculeReadError,
    reports::ReportBuildError,
    structure::StructureReadError,
};
pub(crate) use classification::{classify_validation_code, classify_verification_code};
use serde::Serialize;
use serde_json::Value;
use std::path::PathBuf;

#[derive(Debug)]
pub(crate) enum CliError {
    Core(BioRsError),
    Format(FormatReadError),
    Structure(StructureReadError),
    Molecule(MoleculeReadError),
    Report(ReportBuildError),
    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>,
    },
    ValidationDetails {
        code: &'static str,
        message: String,
        location: Option<String>,
        details: Value,
    },
}

#[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::Format(error) => error.code(),
            Self::Structure(error) => error.code(),
            Self::Molecule(error) => error.code(),
            Self::Report(error) => error.code(),
            Self::ModelInput(ModelInputBuildError::InvalidPolicy { .. }) => {
                "model_input.invalid_policy"
            }
            Self::ModelInput(ModelInputBuildError::InvalidInputHash { .. }) => {
                "workflow.invalid_input_hash"
            }
            Self::ModelInput(ModelInputBuildError::EmptyTokenizedSequence { .. }) => {
                "model_input.invalid_sequence"
            }
            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, .. } | Self::ValidationDetails { code, .. } => code,
        }
    }

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

    pub(crate) fn details(&self) -> Option<&Value> {
        match self {
            Self::ValidationDetails { details, .. } => Some(details),
            _ => None,
        }
    }

    pub(crate) fn recovery_hint(&self) -> Option<&'static str> {
        match self.code() {
            "fasta.missing_header" => {
                Some("Add a FASTA header line starting with '>' before sequence data, then rerun validation.")
            }
            "model_input.invalid_sequence" => {
                Some("Run validation or workflow with the same profile, fix sequence warnings/errors or choose a matching sequence kind/profile, then rerun model-input.")
            }
            "json.invalid" => {
                Some("Provide valid JSON, preferably regenerated by a supported bio-rs command or checked against the documented schema.")
            }
            "io.read_failed" => {
                Some("Check that the local path exists, is readable, and contains valid UTF-8 when a text input is expected.")
            }
            "package.invalid_asset_path" => {
                Some("Use package-relative paths that stay inside the package root; remove absolute paths and '..' traversal.")
            }
            "package.checksum_mismatch" => {
                Some("Confirm the local artifact is the intended file, then regenerate and update the sha256 checksum.")
            }
            "package.invalid_checksum_format" => {
                Some("Use the checksum format 'sha256:' followed by exactly 64 lowercase hexadecimal characters.")
            }
            "package.asset_read_failed" => {
                Some("Create or copy the referenced artifact under the package root, or update the manifest path to the existing file.")
            }
            "package.bridge_not_ready" => {
                Some("Use a supported public package runtime pair such as onnx-webgpu/browser-wasm-webgpu or candle/local-cpu; keep external-process for trusted integration code only.")
            }
            "package.validation_failed" => {
                Some("Inspect error.details.structured_issues, fix each manifest field, then rerun package validation.")
            }
            _ => None,
        }
    }

    pub(crate) const fn exit_code(&self) -> i32 {
        match self {
            Self::Core(_)
            | Self::Format(_)
            | Self::Structure(_)
            | Self::Molecule(_)
            | Self::Report(_)
            | Self::ModelInput(_)
            | Self::Json(_)
            | Self::Validation { .. }
            | Self::ValidationDetails { .. } => 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 },
        }
    }

    pub(crate) fn from_format_read(path: PathBuf, error: FormatReadError) -> Self {
        match error {
            FormatReadError::Io(source) => Self::Read { path, source },
            error => Self::Format(error),
        }
    }

    pub(crate) fn from_structure_read(path: PathBuf, error: StructureReadError) -> Self {
        match error {
            StructureReadError::Io(source) => Self::Read { path, source },
            error => Self::Structure(error),
        }
    }

    pub(crate) fn from_molecule_read(path: PathBuf, error: MoleculeReadError) -> Self {
        match error {
            MoleculeReadError::Io(source) => Self::Read { path, source },
            error => Self::Molecule(error),
        }
    }
}

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::Format(error) => write!(f, "{error}"),
            Self::Structure(error) => write!(f, "{error}"),
            Self::Molecule(error) => write!(f, "{error}"),
            Self::Report(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, .. } | Self::ValidationDetails { 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<ReportBuildError> for CliError {
    fn from(error: ReportBuildError) -> Self {
        Self::Report(error)
    }
}

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