gobby-wiki 0.3.0

Gobby wiki CLI shell
use super::*;

pub(crate) fn select_sources(entries: &[SourceRecord], source_ids: &[String]) -> Selection {
    if source_ids.is_empty() {
        let mut planned = Vec::new();
        let mut skipped = Vec::new();
        let mut failed = Vec::new();
        for record in entries {
            match replay_kind(record) {
                Ok(_) => match RefreshPlan::from_record(record) {
                    Ok(plan) => planned.push(plan),
                    Err(error) => failed.push(refresh_plan_failure(record, error)),
                },
                Err(SelectionFailure::MissingReplayMetadata) => {
                    failed.push(selection_failure(
                        record,
                        SelectionFailure::MissingReplayMetadata,
                    ));
                }
                Err(SelectionFailure::UnsupportedSourceKind) => {
                    skipped.push(SkippedRefresh {
                        id: record.id.clone(),
                        location: record.location.clone(),
                        source_kind: record.kind.clone(),
                        code: "unsupported_source_kind".to_string(),
                        message: format!(
                            "source `{}` has kind `{}` and does not have a refresh replay contract",
                            record.id, record.kind
                        ),
                    });
                }
            }
        }
        return Selection {
            planned,
            skipped,
            failed,
        };
    }

    let mut seen = BTreeSet::new();
    let mut planned = Vec::new();
    let mut failed = Vec::new();
    for id in source_ids {
        if !seen.insert(id.clone()) {
            continue;
        }
        let Some(record) = entries.iter().find(|entry| entry.id == *id) else {
            failed.push(RefreshFailure {
                id: id.clone(),
                location: None,
                source_kind: None,
                code: "not_found".to_string(),
                message: format!("source `{id}` was not found"),
            });
            continue;
        };
        match replay_kind(record) {
            Ok(_) => match RefreshPlan::from_record(record) {
                Ok(plan) => planned.push(plan),
                Err(error) => failed.push(refresh_plan_failure(record, error)),
            },
            Err(error) => {
                failed.push(selection_failure(record, error));
            }
        }
    }

    Selection {
        planned,
        skipped: Vec::new(),
        failed,
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ReplayKind {
    Url,
    LocalFile,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SelectionFailure {
    MissingReplayMetadata,
    UnsupportedSourceKind,
}

pub(crate) fn replay_kind(record: &SourceRecord) -> Result<ReplayKind, SelectionFailure> {
    if is_url_source(record) {
        return Ok(ReplayKind::Url);
    }
    if local_file_replay(record).is_some() {
        return Ok(ReplayKind::LocalFile);
    }
    if is_local_file_source_kind(&record.kind) {
        Err(SelectionFailure::MissingReplayMetadata)
    } else {
        Err(SelectionFailure::UnsupportedSourceKind)
    }
}

pub(crate) fn replay_kind_name(record: &SourceRecord) -> &'static str {
    match replay_kind(record) {
        Ok(ReplayKind::Url) => "url",
        Ok(ReplayKind::LocalFile) => "local_file",
        Err(_) => "unsupported",
    }
}

pub(crate) fn local_file_replay(record: &SourceRecord) -> Option<(&Path, &SourceReplayOptions)> {
    match record.replay.as_ref()? {
        SourceReplay::LocalFile { path, options } => Some((path.as_path(), options)),
    }
}

fn is_local_file_source_kind(kind: &SourceKind) -> bool {
    matches!(
        kind,
        SourceKind::Audio
            | SourceKind::Image
            | SourceKind::Video
            | SourceKind::Pdf
            | SourceKind::Office
            | SourceKind::Html
            | SourceKind::Markdown
            | SourceKind::Text
            | SourceKind::File
    )
}

pub(crate) fn selection_failure(record: &SourceRecord, error: SelectionFailure) -> RefreshFailure {
    match error {
        SelectionFailure::MissingReplayMetadata => RefreshFailure {
            id: record.id.clone(),
            location: Some(record.location.clone()),
            source_kind: Some(record.kind.clone()),
            code: "missing_replay_metadata".to_string(),
            message: format!(
                "source `{}` has kind `{}` but no local replay metadata",
                record.id, record.kind
            ),
        },
        SelectionFailure::UnsupportedSourceKind => RefreshFailure {
            id: record.id.clone(),
            location: Some(record.location.clone()),
            source_kind: Some(record.kind.clone()),
            code: "unsupported_source_kind".to_string(),
            message: format!(
                "source `{}` has kind `{}` and does not have a refresh replay contract",
                record.id, record.kind
            ),
        },
    }
}

fn refresh_plan_failure(record: &SourceRecord, error: WikiError) -> RefreshFailure {
    RefreshFailure {
        id: record.id.clone(),
        location: Some(record.location.clone()),
        source_kind: Some(record.kind.clone()),
        code: "invalid_source_id".to_string(),
        message: error.to_string(),
    }
}

fn is_url_source(record: &SourceRecord) -> bool {
    is_http_url(&record.location) || is_http_url(&record.canonical_location)
}

pub(crate) fn refresh_url(record: &SourceRecord) -> &str {
    if is_http_url(&record.location) {
        &record.location
    } else {
        &record.canonical_location
    }
}

fn is_http_url(value: &str) -> bool {
    let Ok(url) = url::Url::parse(value.trim()) else {
        return false;
    };
    matches!(url.scheme(), "http" | "https") && url.host_str().is_some_and(|host| !host.is_empty())
}