use crate::domain::model::decision_record::DecisionRecord;
use crate::domain::model::event::EventAction;
use crate::domain::model::issue::{Issue, IssueRelationship};
use crate::domain::model::tag_descriptor::TagDescriptors;
use crate::domain::usecases::decision_record::DecisionRecordRepository;
use crate::domain::usecases::issue::{
compute_status_rollup_via_map, compute_tag_rollups, index_issues_by_id, IssueRepository,
};
#[derive(Default)]
pub struct GraphFacts {
pub issues: Vec<(String, String, String)>,
pub decisions: Vec<(String, String, String, String)>,
pub links: Vec<(String, String, String)>,
pub relates: Vec<(String, String)>,
pub assignees: Vec<(String, String)>,
pub tags: Vec<(String, String, String)>,
pub events: Vec<EventFact>,
pub rollups: Vec<RollupFact>,
pub tag_rollups: Vec<TagRollupFact>,
}
pub struct RollupFact {
pub issue_id: String,
pub queued: u32,
pub active: u32,
pub stalled: u32,
pub resolved: u32,
pub cancelled: u32,
pub category: String,
}
pub struct TagRollupFact {
pub issue_id: String,
pub key: String,
pub value: String,
pub count: u32,
}
pub struct EventFact {
pub record_id: String,
pub action: String,
pub ts: String,
pub ts_unix: i64,
pub from_status: Option<String>,
pub to_status: Option<String>,
}
impl GraphFacts {
pub fn from_workspace(
issue_repo: &dyn IssueRepository,
dr_repos: &[(String, &dyn DecisionRecordRepository)],
tag_descriptors: &TagDescriptors,
) -> Self {
let mut facts = Self::default();
facts.absorb_issues(issue_repo, tag_descriptors);
for (kind, dr_repo) in dr_repos {
facts.absorb_decisions(kind, *dr_repo);
}
facts
}
fn absorb_issues(&mut self, repo: &dyn IssueRepository, descriptors: &TagDescriptors) {
let issues: Vec<Issue> = repo.list().unwrap_or_default().into_vec();
let by_id = index_issues_by_id(&issues);
for issue in &issues {
self.absorb_issue(issue);
if let Some(h) = compute_status_rollup_via_map(issue, &by_id) {
self.rollups.push(RollupFact {
issue_id: issue.id.to_string(),
queued: h.queued,
active: h.active,
stalled: h.stalled,
resolved: h.resolved,
cancelled: h.cancelled,
category: h.category().as_str().to_owned(),
});
}
if !descriptors.is_empty() {
let child_refs: Vec<&Issue> = issue
.links
.iter()
.filter(|l| l.relationship == IssueRelationship::ParentOf)
.filter_map(|l| by_id.get(&l.target).copied())
.collect();
for (key, value) in compute_tag_rollups(&child_refs, descriptors) {
self.tag_rollups.push(TagRollupFact {
issue_id: issue.id.to_string(),
key,
value: value.value,
count: value.count,
});
}
}
}
}
fn absorb_issue(&mut self, issue: &Issue) {
let id = issue.id.to_string();
self.issues.push((
id.clone(),
issue.title.as_str().to_owned(),
issue.status.as_str().to_owned(),
));
for link in issue.links.iter() {
self.links.push((
id.clone(),
link.relationship.as_str().to_owned(),
link.target.to_string(),
));
}
for entity_ref in issue.relates().iter() {
self.relates
.push((id.clone(), entity_ref.as_str().to_owned()));
}
if let Some(assignee) = &issue.assignee {
self.assignees
.push((id.clone(), assignee.as_str().to_owned()));
}
absorb_tags(&mut self.tags, &id, issue.tags.iter());
absorb_event_log(&mut self.events, &id, issue.events.iter());
}
fn absorb_decisions(&mut self, kind: &str, repo: &dyn DecisionRecordRepository) {
for record in repo.list().unwrap_or_default() {
self.absorb_decision(kind, &record);
}
}
fn absorb_decision(&mut self, kind: &str, record: &DecisionRecord) {
let id = record.id.to_string();
self.decisions.push((
id.clone(),
kind.to_owned(),
record.title.as_str().to_owned(),
record.status.as_str().to_owned(),
));
for link in record.links.iter() {
self.links.push((
id.clone(),
link.relationship.as_str().to_owned(),
link.target.as_str().to_owned(),
));
}
for entity_ref in record.relates().iter() {
self.relates
.push((id.clone(), entity_ref.as_str().to_owned()));
}
absorb_tags(&mut self.tags, &id, record.tags.iter());
absorb_event_log(&mut self.events, &id, record.events.iter());
}
}
fn absorb_tags<'a, I>(out: &mut Vec<(String, String, String)>, record_id: &str, tags: I)
where
I: IntoIterator<Item = &'a crate::domain::model::tag::Tag>,
{
for tag in tags {
let (key, value) = match tag.as_kv() {
Some((k, v)) => (k.to_owned(), v.to_owned()),
None => (String::new(), tag.as_str().to_owned()),
};
out.push((record_id.to_owned(), key, value));
}
}
fn absorb_event_log<'a, I>(out: &mut Vec<EventFact>, record_id: &str, events: I)
where
I: IntoIterator<Item = &'a crate::domain::model::event::Event>,
{
for event in events {
let ts = event.timestamp.as_str().to_owned();
let ts_unix = event.timestamp.unix_seconds();
let (action, from_status, to_status) = match &event.action {
EventAction::Created { state } => ("created", None, Some(state.as_str().to_owned())),
EventAction::StatusChanged { from, to } => (
"status_changed",
Some(from.as_str().to_owned()),
Some(to.as_str().to_owned()),
),
};
out.push(EventFact {
record_id: record_id.to_owned(),
action: action.to_owned(),
ts,
ts_unix,
from_status,
to_status,
});
}
}