use std::collections::HashMap;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
pub mod load;
use crate::api::browse::{
Advisory, Blame, Blob, BranchInfo, CheckOutcome, Checks, CommitDetail, CommitInfo, Comparison,
DeploymentList, DiscussionDetail, DiscussionList, DiscussionsOf, Gist, GistSummary,
IssueDetail, Job, JobLog, MilestoneDetail, MilestoneList, PrActivity, Profile, Readme, Refs,
Release, RepoOverview, RepoSort, RepoSummary, Results, RunSummary, SearchKind, SearchResults,
StateCounts, TagInfo, TeamDetail, TeamSummary, TreeEntry, UserList, UserSummary, WikiPage,
Workflow, WorkflowRun,
};
use crate::api::commit_comments::CommitComment;
use crate::api::create::NewForm;
use crate::api::edit::Sidebar;
use crate::api::explore::{Topic, Topics};
use crate::api::housekeeping::{BranchRules, FileLook, LabelInfo, Owners, Templates};
use crate::api::insights::{Activity, Dependencies, Traffic};
use crate::api::lists::{ListPage, MyLists, StarList};
use crate::api::manage::{Categories, Target};
use crate::api::mention::Mentionable;
use crate::api::model::{Capped, PrDetail, PrRef, RepoId};
use crate::api::notifications::{Inbox, Unread};
use crate::api::packages::{PackageDetail, PackageList, PackageRef, PackagesOf};
use crate::api::project::{InProject, Project, ProjectRef, ProjectSummary, ProjectsOf};
use crate::api::release::TagLook;
use crate::api::replies::SavedReply;
use crate::api::security::{Alert, AlertKind, AlertList, AlertRef};
use crate::api::settings::{Settings, SettingsPage};
use crate::api::timeline::Review;
use crate::api::watch::{Subscription, Watched};
use crate::create::HeadState;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DataKey {
Repo(RepoId),
Readme(RepoId),
Tree(RepoId, String, String),
Blob(RepoId, String, String),
Search(SearchKind, String),
StateCounts(String),
Issue(RepoId, u64),
Pr(PrRef),
PrActivity(PrRef),
InProjects(RepoId, u64),
Lines(RepoId, String, String),
CommitComments(RepoId, String),
Reviews(PrRef),
Profile(String),
Files(RepoId, String),
Refs(RepoId),
RefIn(RepoId, String),
LastCommits(RepoId, String, String),
Commit(RepoId, String),
History(RepoId, String, String),
PrChecks(PrRef),
Users(UserList),
Forks(RepoId),
Releases(RepoId),
Release(RepoId, String),
Tags(RepoId),
Branches(RepoId),
Wiki(RepoId, Option<String>),
Advisories(Option<RepoId>),
Advisory(Option<RepoId>, String),
Teams(String),
Team(String, String),
Gist(String),
Gists(String),
Blame(RepoId, String, String),
Compare(RepoId, String),
Deployments(RepoId, Option<String>),
Milestones(RepoId, bool),
Milestone(RepoId, u64),
Discussions(DiscussionsOf, Option<String>),
Discussion(DiscussionsOf, u64),
Run(RepoId, u64, Option<u64>),
Job(RepoId, u64),
JobLog(RepoId, u64),
Workflow(RepoId, String),
Runs(RepoId, String),
CommitChecks(RepoId, String),
OwnerRepos(String, RepoSort),
Stars(String),
Notifications(String),
Sidebar(RepoId, u64),
NewForm(RepoId),
Projects(ProjectsOf),
Project(ProjectRef, Option<u64>),
Alerts(RepoId, AlertKind, String),
Alert(AlertRef),
Subscription(Watched),
Settings(RepoId, SettingsPage),
Activity(RepoId),
Traffic(RepoId),
Dependencies(RepoId),
Featured,
Topic(String),
StarLists(String),
StarList(String, String),
MyLists(RepoId),
Packages(PackagesOf),
Package(PackageRef),
SavedReplies,
Labels(RepoId),
Owners,
Templates,
OpenIssues(RepoId),
MoveTargets(String),
Categories(RepoId),
Clone(RepoId),
Head {
repo: RepoId,
base: String,
head: String,
},
TagLook(RepoId, String),
BranchRules(RepoId, String),
FileLook(RepoId, String, String),
Mentionable(RepoId),
Unread,
Suggest(String),
}
impl DataKey {
pub fn kept(&self) -> bool {
!matches!(
self,
DataKey::Blob(..)
| DataKey::JobLog(..)
| DataKey::Files(..)
| DataKey::Lines(..)
| DataKey::Wiki(..)
| DataKey::CommitComments(..)
| DataKey::RefIn(..)
| DataKey::InProjects(..)
| DataKey::Sidebar(..)
| DataKey::NewForm(..)
| DataKey::Subscription(..)
| DataKey::MyLists(..)
| DataKey::Owners
| DataKey::Templates
| DataKey::OpenIssues(_)
| DataKey::MoveTargets(_)
| DataKey::Categories(_)
| DataKey::TagLook(..)
| DataKey::BranchRules(..)
| DataKey::FileLook(..)
| DataKey::Mentionable(_)
| DataKey::Unread
| DataKey::Suggest(_)
| DataKey::Clone(_)
| DataKey::Head { .. }
)
}
pub fn cache_first(&self) -> bool {
!matches!(self, DataKey::Pr(_))
}
fn cache_id(&self) -> Option<String> {
let key = self.kept().then(|| serde_json::to_string(self).ok())??;
Some(format!("data:{key}"))
}
}
pub trait Picked {
fn pick(data: &Data) -> Option<&Self>;
}
impl Picked for Data {
fn pick(data: &Data) -> Option<&Self> {
Some(data)
}
}
macro_rules! data {
($(
$(#[$meta:meta])* $variant:ident($($field:ty),+)
$(=> $picked:ty $([$(.$list:ident)? $id:expr])?)?
$(as $unsized:ty)?,
)*) => {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Data {
$($(#[$meta])* $variant($($field),+),)*
}
$($(
data!(@pick $variant $picked);
impl From<$picked> for Data {
fn from(x: $picked) -> Self {
Data::$variant(x.into())
}
}
)?)*
$($(data!(@pick $variant $unsized);)?)*
impl Data {
fn paged_next(&self) -> Option<&str> {
match self {
$($($(Data::$variant(x) => x$(.$list)?.next.as_deref(),)?)?)*
_ => None,
}
}
fn append_paged(&mut self, more: Data) {
match (self, more) {
$($($(
(Data::$variant(a), Data::$variant(b)) => {
extend(&mut (**a)$(.$list)?, (*b)$(.$list)?, $id);
}
)?)?)*
_ => {}
}
}
}
};
(@pick $variant:ident $kind:ty) => {
impl Picked for $kind {
fn pick(data: &Data) -> Option<&Self> {
match data {
Data::$variant(x) => Some(std::borrow::Borrow::borrow(x)),
_ => None,
}
}
}
};
}
data! {
Repo(Box<RepoOverview>) => RepoOverview,
Readme(Option<Box<Readme>>) => Option<Box<Readme>>,
Tree(Vec<TreeEntry>) as [TreeEntry],
Blob(Box<Blob>) => Blob,
Search(Box<SearchResults>) => SearchResults,
StateCounts(StateCounts) => StateCounts,
Issue(Option<Box<IssueDetail>>) => Option<Box<IssueDetail>>,
Pr(Box<PrDetail>) => PrDetail,
PrActivity(Box<PrActivity>) => PrActivity,
Reviews(Box<Capped<Review>>) => Capped<Review>,
CommitComments(Box<Capped<CommitComment>>) => Capped<CommitComment>,
Lines(Arc<Vec<String>>) => Vec<String>,
InProjects(Box<Capped<InProject>>) => Capped<InProject>,
Profile(Box<Profile>) => Profile,
Files(Arc<Vec<String>>, bool),
Refs(Box<Refs>) => Refs,
RefIn(Option<String>) => Option<String>,
LastCommits(Arc<HashMap<String, CommitInfo>>) => HashMap<String, CommitInfo>,
Commit(Box<CommitDetail>) => CommitDetail,
History(Box<Results<CommitInfo>>) => Results<CommitInfo> [|c| c.oid.clone()],
Checks(Box<Checks>) => Checks,
Users(Box<Results<UserSummary>>) => Results<UserSummary> [|u| u.login.clone()],
RepoPage(Box<Results<RepoSummary>>) => Results<RepoSummary> [|r| r.repo.clone()],
Releases(Box<Results<Release>>) => Results<Release> [|r| r.tag.clone()],
Discussions(Box<DiscussionList>) => DiscussionList [.results |d| d.number],
Discussion(Box<DiscussionDetail>) => DiscussionDetail,
Run(Box<WorkflowRun>) => WorkflowRun,
Job(Box<Job>) => Job,
#[serde(skip)]
Log(Arc<JobLog>) => JobLog,
Workflow(Box<Workflow>) => Workflow,
Runs(Box<Results<RunSummary>>) => Results<RunSummary> [|r| r.id],
Release(Box<Release>) => Release,
Tags(Box<Results<TagInfo>>) => Results<TagInfo> [|t| t.name.clone()],
Branches(Box<Results<BranchInfo>>) => Results<BranchInfo> [|b| b.name.clone()],
Wiki(Box<WikiPage>) => WikiPage,
Advisories(Box<Results<Advisory>>) => Results<Advisory> [|a| a.ghsa.clone()],
Advisory(Box<Advisory>) => Advisory,
Teams(Box<Results<TeamSummary>>) => Results<TeamSummary> [|t| t.slug.clone()],
Team(Box<TeamDetail>) => TeamDetail,
Gist(Box<Gist>) => Gist,
Gists(Box<Results<GistSummary>>) => Results<GistSummary> [|g| g.id.clone()],
Blame(Box<Blame>) => Blame,
Compare(Box<Comparison>) => Comparison,
Deployments(Box<DeploymentList>) => DeploymentList [.results |d| (d.created_at.clone(), d.environment.clone(), d.oid.clone())],
Milestones(Box<MilestoneList>) => MilestoneList [.results |m| m.number],
Milestone(Box<MilestoneDetail>) => MilestoneDetail [.items |i| (i.repo.clone(), i.number)],
Notifications(Box<Inbox>) => Inbox,
#[serde(skip)]
Sidebar(Box<Sidebar>) => Sidebar,
#[serde(skip)]
NewForm(Box<NewForm>) => NewForm,
Projects(Box<Results<ProjectSummary>>) => Results<ProjectSummary> [|p| p.number],
Project(Box<Project>) => Project [.items |i| i.id.clone()],
Alerts(Box<AlertList>) => AlertList [.results |a| a.number],
Alert(Box<Alert>) => Alert,
#[serde(skip)]
Subscription(Box<Subscription>) => Subscription,
Settings(Box<Settings>) => Settings,
Activity(Box<Activity>) => Activity,
Traffic(Box<Traffic>) => Traffic,
Dependencies(Box<Dependencies>) => Dependencies,
Topics(Box<Topics>) => Topics,
Topic(Box<Topic>) => Topic,
StarLists(Box<Capped<StarList>>) => Capped<StarList>,
StarList(Box<ListPage>) => ListPage [.repos |r| r.repo.clone()],
MyLists(Box<MyLists>) => MyLists,
Packages(Box<PackageList>) => PackageList,
Package(Box<PackageDetail>) => PackageDetail [.versions |v| v.id],
SavedReplies(Box<Capped<SavedReply>>) => Capped<SavedReply>,
Labels(Box<Results<LabelInfo>>) => Results<LabelInfo> [|l| l.name.clone()],
Owners(Box<Owners>) => Owners,
Templates(Box<Templates>) => Templates,
#[serde(skip)]
OpenIssues(Box<Capped<(u64, String)>>) => Capped<(u64, String)>,
#[serde(skip)]
MoveTargets(Box<Capped<Target>>) => Capped<Target>,
#[serde(skip)]
Categories(Box<Categories>) => Categories,
#[serde(skip)]
Clone(Option<Box<crate::git::local::Clone>>) => Option<Box<crate::git::local::Clone>>,
#[serde(skip)]
Head(Box<HeadState>) => HeadState,
#[serde(skip)]
TagLook(Box<TagLook>) => TagLook,
#[serde(skip)]
BranchRules(Box<BranchRules>) => BranchRules,
#[serde(skip)]
FileLook(Box<FileLook>) => FileLook,
#[serde(skip)]
Mentionable(Box<Mentionable>) => Mentionable,
#[serde(skip)]
Unread(Unread) => Unread,
#[serde(skip)]
Suggestions(Vec<RepoSummary>) as [RepoSummary],
}
impl Data {
pub fn running(&self) -> bool {
match self {
Data::Log(log) => log.running,
Data::Job(job) => {
job.outcome == CheckOutcome::Pending || job.run.outcome == CheckOutcome::Pending
}
Data::Run(run) => run.outcome == CheckOutcome::Pending,
Data::Checks(c) => c.items.iter().any(|i| i.outcome == CheckOutcome::Pending),
Data::Runs(r) => r.items.iter().any(|r| r.outcome == CheckOutcome::Pending),
_ => false,
}
}
pub fn next_cursor(&self) -> Option<&str> {
match self {
Data::Search(r) => r.next(),
Data::Notifications(i) => i.list.next.as_deref(),
_ => self.paged_next(),
}
}
pub fn append(&mut self, more: Data) {
match (self, more) {
(Data::Search(a), Data::Search(b)) => append(a, *b),
(Data::Notifications(a), Data::Notifications(b)) => {
extend(&mut a.list, b.list, |n| n.id.clone());
a.searched += b.searched;
a.cut = b.cut;
}
(this, more) => this.append_paged(more),
}
}
}
fn extend<T, K: Eq + std::hash::Hash>(a: &mut Results<T>, b: Results<T>, id: impl Fn(&T) -> K) {
let mut seen: std::collections::HashSet<K> = a.items.iter().map(&id).collect();
a.items
.extend(b.items.into_iter().filter(|item| seen.insert(id(item))));
a.next = b.next;
a.total = b.total.max(a.items.len() as u64);
}
fn append(results: &mut SearchResults, more: SearchResults) {
match (results, more) {
(SearchResults::Repos(a), SearchResults::Repos(b)) => extend(a, b, |r| r.repo.clone()),
(SearchResults::Issues(a), SearchResults::Issues(b)) => {
extend(a, b, |i| (i.repo.clone(), i.number));
}
(SearchResults::Users(a), SearchResults::Users(b)) => extend(a, b, |u| u.login.clone()),
(SearchResults::Discussions(a), SearchResults::Discussions(b)) => {
extend(a, b, |d| (d.repo.clone(), d.summary.number));
}
(SearchResults::Commits(a), SearchResults::Commits(b)) => {
extend(a, b, |c| (c.repo.clone(), c.commit.oid.clone()));
}
(SearchResults::Code(a), SearchResults::Code(b)) => {
extend(a, b, |c| (c.repo.clone(), c.path.clone()));
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_next_page_leaves_out_what_is_shown() {
let branch = |n: u32| BranchInfo {
name: format!("b{n}"),
..crate::fixtures::branches().items[0].clone()
};
let page = |names: std::ops::Range<u32>, next: bool| Results {
total: 5,
items: names.map(branch).collect(),
next: next.then(|| "c".to_owned()),
};
let mut shown = Data::from(page(0..3, true));
shown.append(Data::from(page(2..5, false)));
let Data::Branches(r) = shown else { panic!() };
let names: Vec<&str> = r.items.iter().map(|b| b.name.as_str()).collect();
assert_eq!(names, ["b0", "b1", "b2", "b3", "b4"]);
assert_eq!(r.next, None);
}
}