use onetaskgraph_core::{
CopyReport, Predicate, Qualified, QualifiedEdge, QueryPlan, SearchHit, SourceListing,
SourceState,
};
use onetaskgraph_plugin_api::{Capabilities, Document, Label, Location, Project, Support, Task};
use serde::Serialize;
fn wire(value: &impl Serialize) -> String {
serde_json::to_string(value)
.expect("a contract enum serialises")
.trim_matches('"')
.to_owned()
}
fn columns(rows: &[Vec<String>]) -> String {
let width = rows.iter().map(Vec::len).max().unwrap_or(0);
let widths: Vec<usize> = (0..width)
.map(|column| {
rows.iter()
.filter_map(|row| row.get(column))
.map(|cell| cell.chars().count())
.max()
.unwrap_or(0)
})
.collect();
let mut rendered = String::new();
for row in rows {
let last = row.len().saturating_sub(1);
for (index, cell) in row.iter().enumerate() {
if index == last {
rendered.push_str(cell);
} else {
let pad = widths[index].saturating_sub(cell.chars().count());
rendered.push_str(cell);
rendered.push_str(&" ".repeat(pad));
rendered.push_str(" ");
}
}
rendered.push('\n');
}
rendered
}
pub fn tasks(items: &[Qualified<Task>]) -> String {
columns(
&items
.iter()
.map(|task| {
vec![
task.id.to_string(),
wire(&task.item.status.category),
task.item.title.clone(),
]
})
.collect::<Vec<_>>(),
)
}
fn located(location: &Location) -> String {
let rendered = serde_json::to_value(location).expect("a contract enum serialises");
let (kind, place) = rendered
.as_object()
.and_then(|object| object.iter().next())
.expect("an externally tagged enum is an object of exactly one member");
format!("{kind} {}", place.as_str().unwrap_or_default())
}
pub fn documents(items: &[Qualified<Document>]) -> String {
columns(
&items
.iter()
.map(|document| {
vec![
document.id.to_string(),
document
.item
.location
.as_ref()
.map_or_else(|| "-".to_owned(), located),
document.item.title.clone(),
]
})
.collect::<Vec<_>>(),
)
}
pub fn projects(items: &[Qualified<Project>]) -> String {
columns(
&items
.iter()
.map(|project| {
vec![
project.id.to_string(),
wire(&project.item.status.category),
project.item.title.clone(),
]
})
.collect::<Vec<_>>(),
)
}
pub fn copied(report: &CopyReport) -> String {
columns(
&report
.items
.iter()
.map(|outcome| {
vec![
outcome.source.to_string(),
outcome
.destination()
.map_or_else(|| "-".to_owned(), ToString::to_string),
outcome.action.name(),
]
})
.collect::<Vec<_>>(),
)
}
pub fn labels(items: &[Qualified<Label>]) -> String {
columns(
&items
.iter()
.map(|label| vec![label.id.to_string(), label.item.name.clone()])
.collect::<Vec<_>>(),
)
}
pub fn edges(items: &[QualifiedEdge]) -> String {
columns(
&items
.iter()
.map(|edge| {
vec![
format!("{} {}", wire(&edge.from.kind), edge.from.id),
wire(&edge.kind),
format!("{} {}", wire(&edge.to.kind), edge.to.id),
]
})
.collect::<Vec<_>>(),
)
}
pub fn hits(items: &[SearchHit]) -> String {
columns(
&items
.iter()
.map(|hit| match hit {
SearchHit::Task(task) => vec![
"task".to_owned(),
task.id.to_string(),
task.item.title.clone(),
],
SearchHit::Project(project) => vec![
"project".to_owned(),
project.id.to_string(),
project.item.title.clone(),
],
})
.collect::<Vec<_>>(),
)
}
pub fn task_detail(task: &Qualified<Task>) -> String {
let item = &task.item;
let mut fields = vec![
("id", task.id.to_string()),
("title", item.title.clone()),
(
"status",
format!("{} ({})", wire(&item.status.category), item.status.name),
),
];
fields.push((
"project",
match &item.project {
Some(project) => format!("{}:{project}", task.id.source),
None => "none".to_owned(),
},
));
detail(
&mut fields,
&item.labels,
item.url.as_deref(),
item.location.as_ref(),
);
body(&fields, item.content.as_deref())
}
pub fn document_detail(document: &Qualified<Document>) -> String {
let item = &document.item;
let mut fields = vec![
("id", document.id.to_string()),
("title", item.title.clone()),
(
"project",
match &item.project {
Some(project) => format!("{}:{project}", document.id.source),
None => "none".to_owned(),
},
),
];
detail(
&mut fields,
&item.labels,
item.url.as_deref(),
item.location.as_ref(),
);
body(&fields, item.content.as_deref())
}
pub fn project_detail(project: &Qualified<Project>) -> String {
let item = &project.item;
let mut fields = vec![
("id", project.id.to_string()),
("title", item.title.clone()),
(
"status",
format!("{} ({})", wire(&item.status.category), item.status.name),
),
];
detail(
&mut fields,
&item.labels,
item.url.as_deref(),
item.location.as_ref(),
);
body(&fields, item.content.as_deref())
}
fn detail(
fields: &mut Vec<(&'static str, String)>,
item_labels: &[Label],
url: Option<&str>,
location: Option<&Location>,
) {
if !item_labels.is_empty() {
fields.push((
"labels",
item_labels
.iter()
.map(|label| label.name.clone())
.collect::<Vec<_>>()
.join(", "),
));
}
if let Some(url) = url {
fields.push(("url", url.to_owned()));
}
if let Some(location) = location {
fields.push(("location", located(location)));
}
}
fn body(fields: &[(&'static str, String)], content: Option<&str>) -> String {
let mut rendered = columns(
&fields
.iter()
.map(|(name, value)| vec![format!("{name}:"), value.clone()])
.collect::<Vec<_>>(),
);
if let Some(content) = content.map(str::trim).filter(|body| !body.is_empty()) {
rendered.push('\n');
rendered.push_str(content);
rendered.push('\n');
}
rendered
}
pub fn sources(listings: &[SourceListing]) -> String {
columns(
&listings
.iter()
.map(|listing| {
vec![
listing.source.to_string(),
listing.kind.clone(),
match &listing.state {
SourceState::Available { capabilities } => declared(capabilities),
SourceState::Unavailable { error } => {
format!("unavailable — {error}")
}
},
]
})
.collect::<Vec<_>>(),
)
}
fn declared(capabilities: &Capabilities) -> String {
let native: Vec<&str> = [
("label", capabilities.filter_by_label),
("status", capabilities.filter_by_status),
("search-title", capabilities.search_title),
("search-content", capabilities.search_content),
("project", capabilities.projects),
("orphan-tasks", capabilities.orphan_tasks),
]
.into_iter()
.filter(|(_, support)| *support == Support::Native)
.map(|(name, _)| name)
.collect();
format!(
"native: {}; deps: task {}, project {}; page <= {}",
if native.is_empty() {
"none".to_owned()
} else {
native.join(", ")
},
wire(&capabilities.task_dependencies),
wire(&capabilities.project_dependencies),
capabilities.max_page_size,
)
}
pub fn plan(plan: &QueryPlan) -> String {
let mut rendered = String::from("plan:\n");
if plan.per_source.is_empty() {
rendered.push_str(" (no source was addressed)\n");
return rendered;
}
for source in &plan.per_source {
rendered.push_str(&format!(
" {} ({}) {} page(s)\n",
source.source, source.kind, source.pages_fetched
));
for (label, predicates) in [
("pushed down", &source.pushed_down),
("applied locally", &source.applied_locally),
("emulated", &source.emulated),
("unavailable", &source.unavailable),
] {
if predicates.is_empty() {
continue;
}
rendered.push_str(&format!(" {label}: {}\n", predicate_list(predicates)));
}
}
rendered
}
fn predicate_list(predicates: &[Predicate]) -> String {
predicates.iter().map(wire).collect::<Vec<_>>().join(", ")
}