packtrack 3.0.1

A simple CLI for tracking mail packages
Documentation
use crate::cache::{Cache, FileCache};

use crate::cached_tracker::CachedTracker;
use crate::error::Result;
use crate::tracker::Package;
use crate::tracker::TrackerContext;
use crate::tracker::get_handler;
use crate::url_store::AnnotatedUrl;
use tokio::sync::Mutex;

/// Container for settings and runtime flags
pub struct Context {
    /// Max age for cache entries to be reused
    pub cache_seconds:      usize,
    /// If false, don't use the cache at all, even for delivered packages
    pub use_cache:          bool,
    pub filters:            Filters,
    // ----- user preferences -----
    pub default_postcode:   Option<String>,
    pub preferred_language: String,
}
impl Default for Context {
    fn default() -> Self {
        Self {
            // TODO: Maybe make a separate Language enum which implements
            // default = "en"
            preferred_language: "en".to_string(),
            cache_seconds:      0,
            use_cache:          true,
            filters:            Filters::default(),
            default_postcode:   None,
        }
    }
}

#[derive(Default)]
pub struct Filters {
    /// Either a new URL, or a fragment of an existing URL
    pub url:       Option<String>,
    pub sender:    Option<String>,
    /// postal carrier e.g. DHL
    pub carrier:   Option<String>,
    pub recipient: Option<String>,
}

// TODO: This should probably be a custom error
pub struct Job {
    pub url:    AnnotatedUrl,
    pub result: Result<Package>,
}

/// Get the Tracker implementation for the given URL, and track the package.
pub async fn track_url(
    url: &AnnotatedUrl,
    cache: &Mutex<dyn Cache>,
    ctx: &Context,
) -> Job {
    let tracker = match get_handler(&url.url) {
        Ok(tracker) => tracker,
        Err(err) => {
            return Job {
                url:    url.clone(),
                result: Err(err),
            };
        }
    };
    let mut tracker = CachedTracker {
        tracker: tracker,
        cache:   cache,
    };
    let tracker_context = TrackerContext {
        recipient_postcode: ctx.default_postcode.as_deref(),
        language:           &ctx.preferred_language,
    };
    let result = tracker
        .track(&url.url, ctx.cache_seconds, ctx.use_cache, &tracker_context)
        .await;
    Job {
        url: url.clone(),
        result,
    }
}

/// Track all the given URLs asynchronously
pub async fn track_urls(
    urls: Vec<AnnotatedUrl>,
    cache: FileCache,
    ctx: &Context,
) -> Result<Vec<Job>> {
    // fire off all the tasks in parallel
    let cache = Mutex::new(cache);
    let tasks: Vec<_> = urls
        .iter()
        .map(|url| track_url(url, &cache, ctx))
        .collect();
    let mut jobs = futures::future::join_all(tasks).await;
    {
        let cache = cache.lock().await;
        if cache.modified {
            cache.save()?;
        }
    }

    if let Some(query) = &ctx.filters.recipient {
        jobs = jobs
            .into_iter()
            .filter(|job| match &job.result {
                Ok(package) => match package.recipient.as_ref() {
                    Some(recipient) => recipient
                        .to_lowercase()
                        .contains(&query.to_lowercase()),
                    None => false,
                },
                Err(_) => true, // don't remove errors
            })
            .collect();
    }
    if let Some(query) = &ctx.filters.sender {
        jobs = jobs
            .into_iter()
            .filter(|job| match &job.result {
                Ok(package) => match package.sender.as_ref() {
                    Some(sender) => sender
                        .to_lowercase()
                        .contains(&query.to_lowercase()),
                    None => false,
                },
                Err(_) => true, // don't remove errors
            })
            .collect();
    }
    if let Some(query) = &ctx.filters.carrier {
        jobs = jobs
            .into_iter()
            .filter(|job| match &job.result {
                Ok(package) => package
                    .channel
                    .to_lowercase()
                    .contains(&query.to_lowercase()),
                Err(_) => true, // don't remove errors
            })
            .collect();
    }
    Ok(jobs)
}