candding 0.1.0

Pure candle embeddings for Rust with no ONNX: dense, sparse, multi-vector and rerank models today, image models on the roadmap
Documentation
//! What a failed hub request means for candding: the hub's `X-Error-Code` first, then the status and the step.

use std::error::Error;

use hf_hub::HFError;

use crate::error::CanddingError;

/// `Listing` covers the client, the revision lookup and the file tree; a gated repository lists fine.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Step {
  Listing,
  File,
}

#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Kind {
  Gated,
  Repository,
  Revision,
  Other,
}

/// An anonymous request for a missing repository gets a 401 with no code, so a 401 while listing is no gate.
pub(crate) fn kind(step: Step, status: Option<u16>, code: Option<&str>) -> Kind {
  match (code, status, step) {
    (Some("RevisionNotFound"), ..) => Kind::Revision,
    (Some("RepoNotFound"), ..) => Kind::Repository,
    (Some("GatedRepo"), ..) => Kind::Gated,
    (_, Some(401 | 404), Step::Listing) => Kind::Repository,
    (_, Some(401 | 403), _) => Kind::Gated,
    _ => Kind::Other,
  }
}

pub(crate) fn map(step: Step, error: HFError, id: &str, revision: &str) -> CanddingError {
  let id = id.to_string();
  let revision = revision.to_string();
  let (status, code) = response(&error);
  match error {
    HFError::Io(error) => CanddingError::Io(error),
    HFError::Request { source, .. } => CanddingError::Network {
      id,
      reason: chain(&source),
    },
    HFError::RepoNotFound { context: None, .. } => CanddingError::RepositoryNotFound { id },
    HFError::RevisionNotFound { context: None, .. } => {
      CanddingError::RevisionNotFound { id, revision }
    }
    error => match kind(step, status, code.as_deref()) {
      Kind::Gated => CanddingError::GatedModel { id },
      Kind::Repository => CanddingError::RepositoryNotFound { id },
      Kind::Revision => CanddingError::RevisionNotFound { id, revision },
      Kind::Other => CanddingError::DownloadFailed {
        id,
        reason: error.to_string(),
      },
    },
  }
}

/// The status and error code of the response behind `error`, when there was one.
fn response(error: &HFError) -> (Option<u16>, Option<String>) {
  let context = match error {
    HFError::Http { context }
    | HFError::AuthRequired { context }
    | HFError::Forbidden { context }
    | HFError::Conflict { context }
    | HFError::RateLimited { context, .. } => Some(context.as_ref()),
    HFError::RepoNotFound { context, .. }
    | HFError::RevisionNotFound { context, .. }
    | HFError::EntryNotFound { context, .. } => context.as_deref(),
    _ => None,
  };
  (
    context.map(|context| context.status.as_u16()),
    context.and_then(|context| context.error_code.clone()),
  )
}

fn chain(error: &dyn Error) -> String {
  let mut text = error.to_string();
  let mut source = error.source();
  while let Some(cause) = source {
    text.push_str(": ");
    text.push_str(&cause.to_string());
    source = cause.source();
  }
  text
}