#[allow(clippy::wildcard_imports)]
use super::*;
pub(super) fn roadmap_list(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let filter = match roadmap_filter(args) {
Ok(filter) => filter,
Err(()) => return Ok(proto_error()),
};
let (config, _path) = match registry_config(store) {
Ok(loaded) => loaded,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let projection = match project_roadmap(&config, store, &filter) {
Ok(projection) => projection,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let mut lines: Vec<String> = Vec::new();
if let Some(line) = cmd_roadmap::roadmap_filter_line(&filter) {
lines.push(line);
}
lines.extend(cmd_roadmap::roadmap_list_lines(&projection));
let structured = json!({
"filters": serde_json::to_value(&filter).unwrap_or(Value::Null),
"total": projection.total,
"initiatives": projection
.initiatives
.iter()
.map(roadmap_row_value)
.collect::<Vec<Value>>(),
"diagnostics": projection
.diagnostics
.iter()
.map(roadmap_diagnostic_value)
.collect::<Vec<Value>>(),
});
Ok(success_result(lines.join("\n"), structured))
}
pub(super) fn roadmap_show(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let reference = req_string(args, "initiative")?;
let (config, _path) = match registry_config(store) {
Ok(loaded) => loaded,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let row = match roadmap_row_outcome(&config, store, &reference) {
Ok(row) => row,
Err(outcome) => return outcome,
};
let text = cmd_roadmap::roadmap_row_lines(&row).join("\n");
Ok(success_result(text, roadmap_row_value(&row)))
}
pub(super) fn roadmap_summary(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let reference = req_string(args, "initiative")?;
let (config, _path) = match registry_config(store) {
Ok(loaded) => loaded,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let row = match roadmap_row_outcome(&config, store, &reference) {
Ok(row) => row,
Err(outcome) => return outcome,
};
let text = cmd_roadmap::roadmap_row_lines(&row).join("\n");
Ok(success_result(text, initiative_summary_value(&row.summary)))
}
pub(super) fn dispatch(
config: &Config,
store: &Store,
name: &str,
arguments: &Value,
) -> Option<ToolOutcome> {
match name {
"roadmap_list" => Some(roadmap_list(config, store, arguments)),
"roadmap_show" => Some(roadmap_show(config, store, arguments)),
"roadmap_summary" => Some(roadmap_summary(config, store, arguments)),
_ => None,
}
}
pub(super) fn project_roadmap(
config: &Config,
store: &Store,
filter: &RoadmapFilter,
) -> TixResult<RoadmapProjection> {
filter.validate()?;
let issues = store.list_all()?;
let sources = cmd_roadmap::build_sources(config, store, &issues)?;
crate::roadmap::project(config, &sources, filter)
}
pub(super) fn roadmap_row_by_id(
config: &Config,
store: &Store,
id: &str,
) -> TixResult<Option<RoadmapRow>> {
let projection = project_roadmap(config, store, &RoadmapFilter::default())?;
Ok(projection.initiatives.into_iter().find(|row| row.id == id))
}
pub(super) fn association_kind_name(kind: AssociationKind) -> &'static str {
match kind {
AssociationKind::Project => "project",
AssociationKind::Epic => "epic",
AssociationKind::Milestone => "milestone",
}
}
pub(super) fn association_kind(raw: &str) -> Option<AssociationKind> {
match raw.trim() {
"project" => Some(AssociationKind::Project),
"epic" => Some(AssociationKind::Epic),
"milestone" => Some(AssociationKind::Milestone),
_ => None,
}
}
pub(super) fn loose_priority(value: &str) -> crate::model::Priority {
value
.parse()
.unwrap_or_else(|never: std::convert::Infallible| match never {})
}
pub(super) fn initiative_priority(value: Option<String>) -> Option<crate::model::Priority> {
let trimmed = value?;
let trimmed = trimmed.trim();
if trimmed.is_empty() {
None
} else {
Some(loose_priority(trimmed))
}
}
pub(super) fn initiative_text_patch(value: Option<String>) -> crate::projects::Patch<String> {
use crate::projects::Patch;
match value {
None => Patch::Unchanged,
Some(text) if text.trim().is_empty() => Patch::Clear,
Some(text) => Patch::Set(text.trim().to_string()),
}
}
pub(super) fn initiative_reference_patch(
value: Option<String>,
) -> crate::projects::Patch<Vec<String>> {
use crate::projects::Patch;
match value {
None => Patch::Unchanged,
Some(text) if text.trim().is_empty() => Patch::Clear,
Some(text) => Patch::Set(crate::commands::parse_csv_list(&text)),
}
}
pub(super) fn initiative_record_value(id: &str, record: &InitiativeConfiguration) -> Value {
json!({
"id": id,
"name": record.name,
"outcome": record.outcome,
"owner": record.owner,
"status": record.status,
"priority": record.priority,
"labels": record.labels,
"target_date": record.target_date,
"projects": record.projects,
"epics": record.epics,
"milestones": record.milestones,
})
}
pub(super) fn roadmap_diagnostic_value(diagnostic: &RoadmapDiagnostic) -> Value {
json!({
"kind": diagnostic.kind.as_str(),
"initiative": diagnostic.initiative,
"field": diagnostic.field,
"reference": diagnostic.reference,
"source": diagnostic.source,
"message": diagnostic.message,
"remedy": diagnostic.remedy,
})
}
pub(super) fn row_reference_value(reference: &RowReference) -> Value {
json!({
"kind": association_kind_name(reference.kind),
"target": reference.target,
"label": reference.label,
"resolution": reference.resolution.as_str(),
})
}
pub(super) fn health_rollup_value(rollup: &crate::health::HealthRollup) -> Value {
json!({
"scope": rollup.scope.to_string(),
"state": rollup.state.as_str(),
"source": rollup.source.as_str(),
"policy_version": rollup.policy_version,
"contributions": rollup
.contributions
.iter()
.map(|contribution| json!({
"issue": contribution.number,
"state": contribution.state.as_str(),
}))
.collect::<Vec<Value>>(),
})
}
pub(super) fn progress_value(progress: &ProgressValue) -> Value {
match progress {
ProgressValue::Project(report) => json!({
"source": "project",
"value": project_health_value(report),
}),
ProgressValue::Epic(rollup) => json!({
"source": "epic",
"value": epic_rollup_value(rollup),
}),
}
}
pub(super) fn contribution_value(contribution: &SummaryContribution) -> Value {
json!({
"source_entity": contribution.source_entity(),
"kind": association_kind_name(contribution.kind),
"target": contribution.target,
"label": contribution.label,
"resolution": contribution.resolution.as_str(),
"progress": contribution.progress.as_ref().map(progress_value),
"risk": contribution.risk.as_ref().map(health_rollup_value),
"policy_version": contribution.policy_version,
})
}
pub(super) fn initiative_summary_value(summary: &InitiativeSummary) -> Value {
json!({
"initiative_id": summary.initiative_id,
"state": summary.state.as_str(),
"knowable_contributions": summary.knowable_contributions(),
"source_entities": summary.source_entities,
"policy_versions": summary.policy_versions,
"contributions": summary
.contributions
.iter()
.map(contribution_value)
.collect::<Vec<Value>>(),
"diagnostics": summary
.diagnostics
.iter()
.map(roadmap_diagnostic_value)
.collect::<Vec<Value>>(),
})
}
pub(super) fn roadmap_row_value(row: &RoadmapRow) -> Value {
json!({
"id": row.id,
"name": row.name,
"outcome": row.outcome,
"owner": row.owner,
"status": row.status,
"priority": row.priority,
"labels": row.labels,
"target_date": row.target_date,
"references": row.references.iter().map(row_reference_value).collect::<Vec<Value>>(),
"summary": initiative_summary_value(&row.summary),
"diagnostics": row.diagnostics.iter().map(roadmap_diagnostic_value).collect::<Vec<Value>>(),
})
}
pub(super) fn roadmap_filter(args: &Value) -> std::result::Result<RoadmapFilter, ()> {
Ok(RoadmapFilter {
period_from: opt_string(args, "period_from")?,
period_to: opt_string(args, "period_to")?,
owner: opt_string(args, "owner")?,
status: opt_string(args, "status")?,
labels: opt_string_array(args, "labels")?.unwrap_or_default(),
query: opt_string(args, "query")?,
})
}
pub(super) fn initiative_association(store: &Store, args: &Value, associate: bool) -> ToolOutcome {
let reference = req_string(args, "initiative")?;
let raw_kind = req_string(args, "kind")?;
let Some(kind) = association_kind(&raw_kind) else {
return Ok(error_result(format!(
"invalid input: `{raw_kind}` is not an association kind; expected project, epic or \
milestone"
)));
};
let target = req_string(args, "target")?;
let (mut config, path) = match registry_config(store) {
Ok(loaded) => loaded,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let id = match crate::roadmap::registry::resolve_initiative_id(&config, &reference) {
Ok(id) => id,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let outcome = if associate {
crate::roadmap::registry::associate(&mut config, &path, id.as_str(), kind, &target)
} else {
crate::roadmap::registry::dissociate(&mut config, &path, id.as_str(), kind, &target)
};
match outcome {
Ok(changed) => {
let text = if associate {
if changed {
format!("Associated {kind} {target} with initiative {id}")
} else {
format!("Already associated: {kind} {target} with initiative {id}")
}
} else if changed {
format!("Dereferenced {kind} {target} from initiative {id}")
} else {
format!("Not associated: {kind} {target} with initiative {id}")
};
Ok(success_result(
text,
json!({
"initiative": id.as_str(),
"kind": association_kind_name(kind),
"target": target,
"changed": changed,
}),
))
}
Err(e) => Ok(error_result(crate::error::render_error(&e))),
}
}
pub(super) fn roadmap_row_outcome(
config: &Config,
store: &Store,
reference: &str,
) -> ToolOutcome2<RoadmapRow> {
let id = match crate::roadmap::registry::resolve_initiative_id(config, reference) {
Ok(id) => id,
Err(e) => return Err(Ok(error_result(crate::error::render_error(&e)))),
};
match roadmap_row_by_id(config, store, id.as_str()) {
Ok(Some(row)) => Ok(row),
Ok(None) => Err(Ok(error_result(format!(
"internal error: the roadmap projection returned no row for `{id}`"
)))),
Err(e) => Err(Ok(error_result(crate::error::render_error(&e)))),
}
}