use std::{ path::PathBuf, fs::{ self, File } }; use sqlx::prelude::*; use atom_syndication::*; use anyhow::{ Result, Context }; use futures::{ Stream, StreamExt }; use chrono::{ Utc, TimeZone }; use tracing::info; use crate::{ Conn, query::{ self, repo_id } }; #[allow(dead_code)] #[derive(sqlx::FromRow)] struct Issue { number: i64, state: i64, title: String, body: String, user_login: String, html_url: String, updated_at: i64 } async fn query_issues_for_label<'conn>(conn: &'conn mut Conn, repo_id: i64, label: &str) -> impl Stream> + 'conn { sqlx::query_as::<_, Issue>(r#" SELECT issues.number, state, title, body, user_login, html_url, updated_at FROM issues INNER JOIN is_labeled ON is_labeled.issue=issues.number WHERE is_labeled.label=(SELECT id FROM labels WHERE repo=? AND name=?) ORDER BY issues.number DESC "#).bind(repo_id).bind(label) .fetch(conn) } async fn issue_to_entry(conn: &mut Conn, repo_id: i64, issue: Issue) -> Result { let state_label = query::issues::integer_to_state_desc(issue.state); let labels_of_issue = sqlx::query_as::<_, (String,)>( "SELECT labels.name FROM is_labeled JOIN labels ON is_labeled.label=labels.id JOIN issues ON (is_labeled.issue=issues.number AND is_labeled.repo=issues.repo) WHERE is_labeled.repo=? AND is_labeled.issue=?" ).bind(repo_id).bind(issue.number) .fetch(&mut *conn); let all_labels = futures::stream::iter(state_label) .chain(labels_of_issue .filter_map(|row| async { row.ok() }) .map(|(name,)| name)) .map(|name| Category { term: name, scheme: None, label: None }) .collect::>() .await; Ok(EntryBuilder::default() .title(issue.title) .id(issue.html_url.clone()) .updated(Utc.timestamp(issue.updated_at, 0)) .authors(vec![ Person { uri: Some(format!("https://github.com/{}", issue.user_login)), name: issue.user_login, email: None } ]) .categories(all_labels) .links(vec![LinkBuilder::default() .href(issue.html_url) .build() .expect("Failed to build link")]) .content(ContentBuilder::default() .content_type(Some(String::from("html"))) .value(issue.body) .build() .expect("Failed to build content")) .build() .map_err(|err_str| anyhow::anyhow!(err_str)) .context("Failed to build atom entry")?) } pub async fn generate(mut conn: &mut Conn, (ref owner, ref name): (String, String), out_path: PathBuf, labels: Vec) -> Result<()> { let labels = if labels.is_empty() { sqlx::query_as::<_, (String,)>( "SELECT name FROM labels WHERE repo=(SELECT id FROM repositories WHERE owner=? AND name=?)" ).bind(owner).bind(name) .fetch(&mut *conn) .filter_map(|row| async { match row { Ok((label,)) => Some(label), _ => None } }) .collect() .await } else { labels }; let repo_id = repo_id(&mut conn, owner, name).await?; for label in labels { info!("atom for {:?}", label); let mut feed = FeedBuilder::default(); feed.title(label.clone()); let issues: Vec = query_issues_for_label(&mut conn, repo_id, &label).await .filter_map(|res| async { res.ok() }) .collect().await; let entries: Vec = { let mut acc = Vec::new(); for issue in issues.into_iter() { acc.push(issue_to_entry(&mut conn, repo_id, issue).await?); } acc }; feed.entries(entries); let feed = feed.build().expect("Failed to build feed"); let feed_directory = out_path.join(label); fs::create_dir_all(&feed_directory)?; let feed_path = feed_directory.join("atom.xml"); let mut out_file = File::create(feed_path)?; feed.write_to(&mut out_file)?; } Ok(()) }