use super::balls::{Ball, Status, ladder};
use crate::binding::{Workspace, WorkspaceKind};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JoinStatus {
Ready,
Blocked,
Claimed,
Closed,
}
fn live_status(status: Status) -> JoinStatus {
match status {
Status::Ready => JoinStatus::Ready,
Status::Blocked => JoinStatus::Blocked,
Status::Claimed => JoinStatus::Claimed,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JoinState {
ReadyStartable,
Blocked,
Bound,
ClaimedElsewhere,
Delivered,
UnassignedWorkspace,
OrphanedProject,
}
pub fn classify(status: JoinStatus, bound: bool) -> JoinState {
match (status, bound) {
(JoinStatus::Ready, _) => JoinState::ReadyStartable,
(JoinStatus::Blocked, _) => JoinState::Blocked,
(JoinStatus::Claimed, true) => JoinState::Bound,
(JoinStatus::Claimed, false) => JoinState::ClaimedElsewhere,
(JoinStatus::Closed, _) => JoinState::Delivered,
}
}
pub fn badge(state: JoinState, claimant: Option<&str>) -> Option<String> {
Some(match state {
JoinState::ReadyStartable | JoinState::Bound | JoinState::UnassignedWorkspace => {
return None;
}
JoinState::Blocked => "blocked".to_owned(),
JoinState::ClaimedElsewhere => format!("claimed by {}", claimant.unwrap_or("?")),
JoinState::Delivered => "delivered".to_owned(),
JoinState::OrphanedProject => "project missing".to_owned(),
})
}
pub fn owner_name(row: &JoinRow) -> String {
row.claimant.clone().unwrap_or_default()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JoinRow {
pub project: String,
pub ball_id: String,
pub state: JoinState,
pub workspace: Option<String>,
pub claimant: Option<String>,
pub title: Option<String>,
}
fn named_map(workspaces: &[Workspace]) -> BTreeMap<&str, &Path> {
workspaces
.iter()
.filter_map(|w| match &w.kind {
WorkspaceKind::Named { name } => Some((name.as_str(), w.path.as_path())),
_ => None,
})
.collect()
}
pub fn join(
projects: &[PathBuf],
cloned: &[PathBuf],
live_by_project: &HashMap<PathBuf, Vec<Ball>>,
closed_by_project: &HashMap<PathBuf, Vec<Ball>>,
workspaces: &[Workspace],
) -> Vec<JoinRow> {
let names = named_map(workspaces);
let mut rows = Vec::new();
let mut engaged: HashSet<String> = HashSet::new();
for project in cloned {
let name = crate::naming::name_of(projects, project);
let Some(live) = live_by_project.get(project) else {
rows.push(orphaned_row(name));
continue;
};
let live_ids: HashSet<&str> = live.iter().map(|b| b.id.as_str()).collect();
for ball in live {
let bound = bind(&names, ball, &mut engaged);
let js = live_status(ladder(ball, &live_ids));
rows.push(JoinRow {
project: name.clone(),
ball_id: ball.id.clone(),
state: classify(js, bound.is_some()),
workspace: bound,
claimant: ball.claimant.clone(),
title: Some(ball.title.clone()),
});
}
for ball in closed_by_project.get(project).into_iter().flatten() {
if live_ids.contains(ball.id.as_str()) {
continue;
}
if let Some(bound) = bind(&names, ball, &mut engaged) {
rows.push(JoinRow {
project: name.clone(),
ball_id: ball.id.clone(),
state: classify(JoinStatus::Closed, true),
workspace: Some(bound),
claimant: ball.claimant.clone(),
title: None,
});
}
}
}
for name in names.keys() {
if !engaged.contains(*name) {
rows.push(JoinRow {
project: String::new(),
ball_id: String::new(),
state: JoinState::UnassignedWorkspace,
workspace: Some((*name).to_owned()),
claimant: None,
title: None,
});
}
}
rows
}
fn bind(
names: &BTreeMap<&str, &Path>,
ball: &Ball,
engaged: &mut HashSet<String>,
) -> Option<String> {
let claimant = ball.claimant.as_deref()?;
names.get(claimant)?;
engaged.insert(claimant.to_owned());
Some(claimant.to_owned())
}
fn orphaned_row(project: String) -> JoinRow {
JoinRow {
project,
ball_id: String::new(),
state: JoinState::OrphanedProject,
workspace: None,
claimant: None,
title: None,
}
}
#[cfg(test)]
mod tests;