odl 0.5.2

flexible download library and CLI intended to be fast, reliable, and easy to use.
Documentation
use prost::DecodeError;
use std::error::Error;
use thiserror::Error;
use tokio::{sync::AcquireError, task::JoinError};

use crate::{
    config::{ConfigBuilderError, DownloadOptionsBuilderError},
    conflict::{SaveConflict, ServerConflict},
};

#[derive(Error, Debug)]
pub enum NetworkError {
    #[error("Connection error")]
    Connect,
    #[error("Connection timeout")]
    Timeout,
    #[error("Response body error")]
    ResponseBody,
    #[error("Response status not successful: {status_code}")]
    Status { status_code: u16 },
    #[error("Network error: {message}")]
    Other { message: String },
}

#[derive(Error, Debug)]
pub enum ConflictError {
    #[error("Save conflict: {conflict}")]
    Save { conflict: SaveConflict },
    #[error("Server conflict: {conflict}")]
    Server { conflict: ServerConflict },
    #[error("Checksum mismatch: expected `{expected}`, got `{actual}`")]
    ChecksumMismatch { expected: String, actual: String },
}

#[derive(Error, Debug)]
pub enum MetadataError {
    #[error("metadata lock is held by another process (lockfile in use)")]
    LockfileInUse,
    #[error("failed to decode metadata: {e}")]
    MetadataDecodeError { e: DecodeError },
    #[error("metadata error: {message}")]
    Other { message: String },
}

#[derive(Error, Debug)]
pub enum OdlError {
    #[error(transparent)]
    Network(#[from] NetworkError),
    #[error(transparent)]
    Conflict(#[from] ConflictError),
    #[error("The input file is empty")]
    EmptyInputFile,
    #[error("URL decode error: {message}")]
    UrlDecodeError { message: String },
    #[error("I/O error: {e} {extra_info:?}")]
    StdIoError {
        e: std::io::Error,
        extra_info: Option<String>,
    },
    #[error("CLI error: {message}")]
    CliError { message: String },
    #[error("download cancelled")]
    Cancelled,
    #[error(transparent)]
    ConfigBuilderError(#[from] ConfigBuilderError),
    #[error(transparent)]
    DownloadOptionsBuilderError(#[from] DownloadOptionsBuilderError),
    #[error(transparent)]
    MetadataError(#[from] MetadataError),
    #[error("{message}")]
    Other {
        message: String,
        origin: Box<dyn std::error::Error + Send + Sync>,
    },
}

impl From<reqwest::Error> for OdlError {
    fn from(e: reqwest::Error) -> Self {
        if let Some(status) = e.status()
            && !status.is_success()
        {
            return Self::Network(NetworkError::Status {
                status_code: status.as_u16(),
            });
        }
        if e.is_timeout() {
            return OdlError::Network(NetworkError::Timeout);
        }

        if e.is_body() {
            return OdlError::Network(NetworkError::ResponseBody);
        }

        if e.is_connect() {
            return OdlError::Network(NetworkError::Connect);
        }

        if let Some(io_err) = e.source().and_then(|s| s.downcast_ref::<std::io::Error>())
            && io_err.kind() == std::io::ErrorKind::TimedOut
        {
            return OdlError::Network(NetworkError::Timeout);
        }

        Self::Network(NetworkError::Other {
            message: e.to_string(),
        })
    }
}

impl From<std::io::Error> for OdlError {
    fn from(e: std::io::Error) -> Self {
        Self::StdIoError {
            e,
            extra_info: None,
        }
    }
}

impl From<JoinError> for OdlError {
    fn from(e: JoinError) -> Self {
        Self::Other {
            message: e.to_string(),
            origin: Box::new(e),
        }
    }
}

impl From<crate::download::DownloadBuilderError> for OdlError {
    fn from(e: crate::download::DownloadBuilderError) -> Self {
        Self::Other {
            message: e.to_string(),
            origin: Box::new(e),
        }
    }
}

impl From<prost::DecodeError> for OdlError {
    fn from(e: prost::DecodeError) -> Self {
        OdlError::MetadataError(MetadataError::MetadataDecodeError { e })
    }
}

impl From<AcquireError> for OdlError {
    fn from(e: AcquireError) -> Self {
        OdlError::Other {
            message: "failed to acquire semaphore permit".to_string(),
            origin: Box::new(e),
        }
    }
}

impl From<keyring::Error> for OdlError {
    fn from(e: keyring::Error) -> Self {
        OdlError::Other {
            message: e.to_string(),
            origin: Box::new(e),
        }
    }
}

#[derive(Error, Debug)]
pub enum DownloadParseError {
    #[error("Failed to parse URL: {message}")]
    InvalidUrl { message: String },
    #[error("Invalid timestamp")]
    InvalidTimestamp,
}