paperless-api 0.13.0

Async Paperless ngx API client
Documentation
//! Utility types.

use serde::Deserialize;

#[derive(Debug, Clone, Deserialize)]
pub struct Statistics {
    /// Total number of documents.
    pub documents_total: u32,

    /// Number of documents in the inbox.
    pub documents_inbox: u32,

    /// Tag used for documents in the inbox.
    pub inbox_tag: u32,

    /// Tags used for documents in the inbox.
    pub inbox_tags: Vec<u32>,

    /// Counts of document file types.
    pub document_file_type_counts: Vec<DocumentFileTypeCount>,

    /// Total number of characters in all documents.
    pub character_count: u64,

    /// Total number of tags.
    pub tag_count: u32,

    /// Total number of correspondents.
    pub correspondent_count: u32,

    /// Total number of document types.
    pub document_type_count: u32,

    /// Total number of storage paths.
    pub storage_path_count: u32,

    /// Current ASN.
    pub current_asn: u32,
}

#[derive(Debug, Clone, Deserialize)]
pub struct DocumentFileTypeCount {
    /// MIME type of the document type.
    pub mime_type: String,

    /// Number of documents of this type.
    #[serde(rename = "mime_type_count")]
    pub count: u32,
}

#[derive(Debug, Clone, Deserialize)]
pub struct ServerStatus {
    /// The version of Paperless-ngx.
    #[serde(rename = "pngx_version")]
    pub version: String,

    /// Operating system of the server.
    pub server_os: String,

    /// Type of installation (e.g. docker).
    pub install_type: String,

    /// Storage information.
    pub storage: Storage,

    /// Database information.
    pub database: Database,

    /// Task status information.
    pub tasks: StatusTask,
}

/// Storage information for the server.
#[derive(Debug, Clone, Deserialize)]
pub struct Storage {
    /// Total storage in bytes.
    pub total: u64,

    /// Available storage in bytes.
    pub available: u64,
}

/// Database information.
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "RawDatabase")]
pub struct Database {
    /// Type of the database.
    pub db_type: String,

    /// URL of the database.
    pub url: String,

    /// Health status of the database.
    pub status: Health,

    /// Migration status of the database.
    pub migration_status: MigrationStatus,
}

/// Migration status of the database.
#[derive(Debug, Clone, Deserialize)]
pub struct MigrationStatus {
    /// The latest applied migration.
    pub latest_migration: Option<String>,

    /// Unapplied migrations.
    pub unapplied_migrations: Vec<String>,
}

/// Health status of a component.
#[derive(Debug, Clone, Deserialize)]
pub enum Health {
    /// The component is healthy.
    #[serde(rename = "OK")]
    Ok,

    /// The component is not healthy, with an error message.
    #[serde(untagged)]
    NotOk(String),
}

/// Task status information.
#[derive(Debug, Clone, Deserialize)]
pub struct StatusTask {
    #[serde(flatten)]
    pub redis: RedisStatus,

    #[serde(flatten)]
    pub celery: CeleryStatus,

    #[serde(flatten)]
    pub index: IndexStatus,

    #[serde(flatten)]
    pub sanity_check: SanityCheckStatus,

    #[serde(flatten)]
    pub classifier: ClassifierStatus,
}

/// Redis status information.
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "RawRedisStatus")]
pub struct RedisStatus {
    /// URL of the Redis server.
    pub url: String,

    /// Health status of Redis.
    pub status: Health,
}

/// Celery status information.
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "RawCeleryStatus")]
pub struct CeleryStatus {
    /// Health status of Celery.
    pub status: Health,

    /// URL of the Celery broker.
    pub url: String,
}

/// Index status information.
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "RawIndexStatus")]
pub struct IndexStatus {
    /// Health status of the index.
    pub status: Health,

    /// When the index was last modified.
    pub last_modified: Option<chrono::DateTime<chrono::Utc>>,
}

/// Classifier status information.
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "RawClassifierStatus")]
pub struct ClassifierStatus {
    /// Health status of the classifier.
    pub status: Health,

    /// When the classifier was last trained.
    pub last_trained: Option<chrono::DateTime<chrono::Utc>>,
}

/// Sanity check status information.
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "RawSanityCheckStatus")]
pub struct SanityCheckStatus {
    /// Health status of the sanity check.
    pub status: Health,

    /// When the sanity check was last run.
    pub last_run: Option<chrono::DateTime<chrono::Utc>>,
}

#[derive(Deserialize)]
pub struct RawDatabase {
    #[serde(rename = "type")]
    pub db_type: String,
    pub url: String,
    pub status: String,
    pub error: Option<String>,
    pub migration_status: MigrationStatus,
}

#[derive(Deserialize)]
#[allow(clippy::struct_field_names)]
struct RawRedisStatus {
    redis_url: String,
    redis_status: String,
    redis_error: Option<String>,
}

#[derive(Deserialize)]
#[allow(clippy::struct_field_names)]
struct RawCeleryStatus {
    celery_status: String,
    celery_url: String,
    celery_error: Option<String>,
}

#[derive(Deserialize)]
#[allow(clippy::struct_field_names)]
struct RawIndexStatus {
    index_status: String,
    index_last_modified: Option<chrono::DateTime<chrono::Utc>>,
    index_error: Option<String>,
}

#[derive(Deserialize)]
#[allow(clippy::struct_field_names)]
struct RawClassifierStatus {
    classifier_status: String,
    classifier_last_trained: Option<chrono::DateTime<chrono::Utc>>,
    classifier_error: Option<String>,
}

#[derive(Deserialize)]
#[allow(clippy::struct_field_names)]
struct RawSanityCheckStatus {
    sanity_check_status: String,
    sanity_check_last_run: Option<chrono::DateTime<chrono::Utc>>,
    sanity_check_error: Option<String>,
}

impl From<RawDatabase> for Database {
    fn from(raw: RawDatabase) -> Self {
        Self {
            db_type: raw.db_type,
            url: raw.url,
            status: merge_status_with_error(&raw.status, raw.error),
            migration_status: raw.migration_status,
        }
    }
}

impl From<RawRedisStatus> for RedisStatus {
    fn from(raw: RawRedisStatus) -> Self {
        Self {
            url: raw.redis_url,
            status: merge_status_with_error(&raw.redis_status, raw.redis_error),
        }
    }
}

impl From<RawCeleryStatus> for CeleryStatus {
    fn from(raw: RawCeleryStatus) -> Self {
        Self {
            status: merge_status_with_error(&raw.celery_status, raw.celery_error),
            url: raw.celery_url,
        }
    }
}

impl From<RawIndexStatus> for IndexStatus {
    fn from(raw: RawIndexStatus) -> Self {
        Self {
            status: merge_status_with_error(&raw.index_status, raw.index_error),
            last_modified: raw.index_last_modified,
        }
    }
}

impl From<RawClassifierStatus> for ClassifierStatus {
    fn from(raw: RawClassifierStatus) -> Self {
        Self {
            status: merge_status_with_error(&raw.classifier_status, raw.classifier_error),
            last_trained: raw.classifier_last_trained,
        }
    }
}

impl From<RawSanityCheckStatus> for SanityCheckStatus {
    fn from(raw: RawSanityCheckStatus) -> Self {
        Self {
            status: merge_status_with_error(&raw.sanity_check_status, raw.sanity_check_error),
            last_run: raw.sanity_check_last_run,
        }
    }
}

impl std::fmt::Display for Health {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Health::Ok => write!(f, "OK"),
            Health::NotOk(err) => write!(f, "Error: {err}"),
        }
    }
}

fn merge_status_with_error(status: &str, error: Option<String>) -> Health {
    if status.to_lowercase() == "ok" && error.is_none() {
        Health::Ok
    } else {
        Health::NotOk(format!(
            "{status}: {error}",
            error = error.unwrap_or_default()
        ))
    }
}

impl ServerStatus {
    /// Returns `Health::Ok` if all components report `Ok`.
    /// Otherwise returns `Health::NotOk` with a combined message.
    #[must_use]
    pub fn overall(&self) -> Health {
        let components = [
            ("database.status", &self.database.status),
            ("redis", &self.tasks.redis.status),
            ("celery", &self.tasks.celery.status),
            ("index", &self.tasks.index.status),
            ("sanity_check", &self.tasks.sanity_check.status),
            ("classifier", &self.tasks.classifier.status),
        ];

        let errors: Vec<_> = components
            .iter()
            .filter_map(|(name, health)| match health {
                Health::NotOk(err) => Some(format!("{name}: {err}")),
                Health::Ok => None,
            })
            .collect();

        if errors.is_empty() {
            Health::Ok
        } else {
            Health::NotOk(errors.join(", "))
        }
    }
}