use std::collections::HashMap;
use std::str::FromStr;
use std::time::Duration;
use chrono::{DateTime, Local};
use educe::Educe;
use regex::Regex;
use serde::Deserialize;
use self::output::OutputKind;
use self::output::{display, element, mail, mattermost};
use crate::sources::{
data,
list::{self, ListOptions},
};
use crate::util::serde::MapOrString;
use crate::{Error, Result, Results};
pub mod output;
mod process;
#[derive(Debug, Clone)]
pub struct Options {
pub last: Option<Duration>,
pub since: Option<DateTime<Local>>,
pub include: Option<Regex>,
pub exclude: Option<Regex>,
pub outputs: Vec<String>,
}
#[allow(dead_code)]
impl Options {
fn last_or_default(&self) -> Duration {
self.last.unwrap_or_else(|| Duration::from_hours(24))
}
}
pub async fn run(
options: Options,
sources: &[Source],
outputs: HashMap<String, OutputKind>,
) -> Result<()> {
if sources.is_empty() {
tracing::error!("No sources provided");
return Ok(());
}
let mut list_options =
ListOptions::default().since(process::compute_since_date(&options).into());
if let Some(regex) = options.include.as_ref() {
list_options = list_options.pick_when_true(move |name, _| regex.is_match(name));
}
if let Some(regex) = options.exclude.as_ref() {
list_options = list_options.pick_when_true(move |name, _| !regex.is_match(name));
}
let results: Results<VersionedSource> = futures::future::join_all(
sources
.iter()
.map(|source| fetch_source(source, &list_options)),
)
.await
.into();
let (fetched, errors) = results.split();
let failed = errors.len();
for err in errors {
tracing::error!("{:?}", miette::Report::new(err));
}
let sources = process::process_result(&fetched, &options);
if options.outputs.is_empty() {
display::display(&sources);
} else {
for output_name in &options.outputs {
if let Some(output) = outputs.get(output_name) {
match output {
OutputKind::Mattermost(config) => {
mattermost::output_mattermost(&options, &sources, config).await?;
}
OutputKind::Element(config) => {
element::output_element(&options, &sources, config).await?;
}
OutputKind::Mail(config) => {
mail::output_mail(&options, &sources, config).await?;
}
}
} else {
tracing::warn!("Unknown output: {output_name}");
}
}
}
if failed > 0 {
return Err(Error::SourcesFailed { count: failed });
}
Ok(())
}
#[serde_with::serde_as]
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
pub struct Config {
#[serde_as(as = "Vec<MapOrString>")]
#[serde(default)]
pub sources: Vec<Source>,
#[serde(default)]
pub outputs: HashMap<String, OutputKind>,
}
#[serde_with::serde_as]
#[derive(Educe, Debug, Clone, Deserialize)]
#[educe(PartialEq)]
pub struct Source {
pub label: Option<String>,
pub source: url::Url,
#[educe(PartialEq(ignore))]
#[serde_as(as = "Option<serde_with::DisplayFromStr>")]
pub include: Option<Regex>,
#[educe(PartialEq(ignore))]
#[serde_as(as = "Option<serde_with::DisplayFromStr>")]
pub exclude: Option<Regex>,
}
impl Source {
pub fn label_or_source(&self) -> &str {
self.label.as_deref().unwrap_or(self.source.as_str())
}
pub fn parse_source_data(&self) -> Result<data::Source> {
data::Source::parse_url(&self.source)
}
}
impl FromStr for Source {
type Err = url::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (label, source) = match s.split_once("::") {
Some((label, source)) => (Some(label), source),
None => (None, s),
};
Ok(Source {
label: label.map(ToOwned::to_owned),
source: source.parse()?,
include: None,
exclude: None,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct VersionedSource {
source: Source,
versions: Vec<list::Tag>,
}
async fn fetch_source(source: &Source, options: &ListOptions<'_>) -> Result<VersionedSource> {
let data = source.parse_source_data()?;
let mut options = options.cloned();
if let Some(regex) = source.include.as_ref() {
options = options.pick_when_true(move |name, _| regex.is_match(name));
}
if let Some(regex) = source.exclude.as_ref() {
options = options.pick_when_true(move |name, _| !regex.is_match(name));
}
let tags = list::list_tags(&data.into(), &options).await?;
Ok(VersionedSource {
source: source.clone(),
versions: tags,
})
}