upstream-rs 2.6.0

Fetch package updates directly from the source.
Documentation
use std::path::PathBuf;

use anyhow::{Context, Result, anyhow};
use chrono::{DateTime, Utc};
use rusqlite::Row;
use serde::{Serialize, de::DeserializeOwned};

use crate::models::common::Version;
use crate::models::upstream::Package;
use crate::providers::pattern_matcher::PatternTable;

pub const PACKAGE_COLUMNS: &str = "
    name,
    repo_slug,
    filetype,
    version_major,
    version_minor,
    version_patch,
    version_is_prerelease,
    channel,
    provider,
    base_url,
    install_type,
    build_branch,
    build_commit,
    is_pinned,
    icon_path,
    install_path,
    exec_path,
    last_upgraded";

pub(super) fn row_to_package(row: &Row<'_>) -> rusqlite::Result<Package> {
    let version_major: u32 = row.get(3)?;
    let version_minor: u32 = row.get(4)?;
    let version_patch: u32 = row.get(5)?;
    let version_is_prerelease: bool = db_bool(row.get(6)?);
    let last_upgraded: String = row.get(17)?;

    Ok(Package {
        name: row.get(0)?,
        repo_slug: row.get(1)?,
        filetype: enum_from_db_value(row.get::<_, String>(2)?, 2)?,
        version: Version::new(
            version_major,
            version_minor,
            version_patch,
            version_is_prerelease,
        ),
        channel: enum_from_db_value(row.get::<_, String>(7)?, 7)?,
        provider: enum_from_db_value(row.get::<_, String>(8)?, 8)?,
        base_url: row.get(9)?,
        install_type: enum_from_db_value(row.get::<_, String>(10)?, 10)?,
        build_branch: row.get(11)?,
        build_commit: row.get(12)?,
        is_pinned: db_bool(row.get(13)?),
        match_pattern: PatternTable::empty(),
        exclude_pattern: PatternTable::empty(),
        icon_path: optional_path_from_db(row.get(14)?),
        install_path: optional_path_from_db(row.get(15)?),
        exec_path: optional_path_from_db(row.get(16)?),
        last_upgraded: parse_timestamp(last_upgraded, 17)?,
    })
}

pub(super) fn enum_to_db<T>(value: &T) -> Result<String>
where
    T: Serialize,
{
    let serialized =
        serde_json::to_value(value).context("Failed to serialize enum for database")?;
    serialized
        .as_str()
        .map(ToOwned::to_owned)
        .ok_or_else(|| anyhow!("Enum did not serialize to a string"))
}

pub(super) fn optional_path_to_db(path: &Option<PathBuf>) -> Result<Option<String>> {
    path.as_ref()
        .map(|path| {
            path.to_str()
                .map(ToOwned::to_owned)
                .ok_or_else(|| anyhow!("Path '{}' is not valid UTF-8", path.display()))
        })
        .transpose()
}

pub(super) fn bool_to_db(value: bool) -> i64 {
    if value { 1 } else { 0 }
}

fn enum_from_db_value<T>(value: String, column: usize) -> rusqlite::Result<T>
where
    T: DeserializeOwned,
{
    serde_json::from_value(serde_json::Value::String(value)).map_err(|err| {
        rusqlite::Error::FromSqlConversionFailure(
            column,
            rusqlite::types::Type::Text,
            Box::new(err),
        )
    })
}

fn optional_path_from_db(path: Option<String>) -> Option<PathBuf> {
    path.map(PathBuf::from)
}

fn db_bool(value: i64) -> bool {
    value != 0
}

fn parse_timestamp(value: String, column: usize) -> rusqlite::Result<DateTime<Utc>> {
    DateTime::parse_from_rfc3339(&value)
        .map(|timestamp| timestamp.with_timezone(&Utc))
        .map_err(|err| {
            rusqlite::Error::FromSqlConversionFailure(
                column,
                rusqlite::types::Type::Text,
                Box::new(err),
            )
        })
}