use balls::reads::Entry;
use serde_json::Value;
use std::collections::HashSet;
pub use balls::task::Status;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Blocker {
pub id: String,
pub on: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ball {
pub id: String,
pub title: String,
pub body: String,
pub claimant: Option<String>,
pub blockers: Vec<Blocker>,
pub parent: Option<String>,
pub priority: i64,
pub tags: Vec<String>,
pub created: Option<i64>,
pub updated: Option<i64>,
pub root_commit: Option<String>,
}
fn opt_str(obj: &serde_json::Map<String, Value>, key: &str) -> Option<String> {
obj.get(key)
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
.map(str::to_owned)
}
fn blockers_of(obj: &serde_json::Map<String, Value>) -> Vec<Blocker> {
let Some(arr) = obj.get("blockers").and_then(Value::as_array) else {
return Vec::new();
};
arr.iter()
.filter_map(|v| {
let b = v.as_object()?;
Some(Blocker {
id: b.get("id")?.as_str()?.to_owned(),
on: b.get("on")?.as_str()?.to_owned(),
})
})
.collect()
}
fn tags_of(obj: &serde_json::Map<String, Value>) -> Vec<String> {
let Some(arr) = obj.get("tags").and_then(Value::as_array) else {
return Vec::new();
};
arr.iter()
.filter_map(Value::as_str)
.map(str::to_owned)
.collect()
}
fn str_or_empty(obj: &serde_json::Map<String, Value>, key: &str) -> String {
obj.get(key)
.and_then(Value::as_str)
.unwrap_or("")
.to_owned()
}
fn ball_of(v: &Value) -> Option<Ball> {
let obj = v.as_object()?;
Some(Ball {
id: opt_str(obj, "id")?,
title: str_or_empty(obj, "title"),
body: str_or_empty(obj, "body"),
claimant: opt_str(obj, "claimant"),
blockers: blockers_of(obj),
parent: opt_str(obj, "parent"),
priority: obj.get("priority").and_then(Value::as_i64).unwrap_or(0),
tags: tags_of(obj),
created: obj.get("created").and_then(Value::as_i64),
updated: obj.get("updated").and_then(Value::as_i64),
root_commit: opt_str(obj, "root_commit"),
})
}
pub fn parse_list(json: &str) -> Vec<Ball> {
match serde_json::from_str::<Value>(json) {
Ok(Value::Array(items)) => items.iter().filter_map(ball_of).collect(),
_ => Vec::new(),
}
}
impl From<&Entry> for Ball {
fn from(entry: &Entry) -> Self {
let task = &entry.task;
Ball {
id: entry.id.clone(),
title: task.title.clone(),
body: task.body.clone(),
claimant: non_empty(task.claimant.clone()),
blockers: task
.blockers
.iter()
.map(|b| Blocker {
id: b.id.clone(),
on: b.on.token().to_owned(),
})
.collect(),
parent: non_empty(task.parent.clone()),
priority: task.priority.unwrap_or(0),
tags: task.tags.clone(),
created: Some(task.created),
updated: Some(task.updated),
root_commit: non_empty(task.root_commit.clone()),
}
}
}
fn non_empty(v: Option<String>) -> Option<String> {
v.filter(|s| !s.is_empty())
}
pub fn ladder(ball: &Ball, live: &HashSet<&str>) -> Status {
if ball.claimant.is_some() {
Status::Claimed
} else if ball
.blockers
.iter()
.any(|b| b.on == "claim" && live.contains(b.id.as_str()))
{
Status::Blocked
} else {
Status::Ready
}
}
#[cfg(test)]
mod tests;