#[allow(clippy::wildcard_imports)]
use super::*;
pub(super) fn success_result(text: String, structured: Value) -> Value {
json!({
"content": [{ "type": "text", "text": text }],
"structuredContent": structured,
"isError": false,
})
}
pub(super) fn error_result(text: String) -> Value {
json!({
"content": [{ "type": "text", "text": text }],
"isError": true,
})
}
pub(super) fn issue_value(
issue: &Issue,
resolution: &crate::workflow::CategoryResolution,
) -> Value {
let mut obj = serde_json::to_value(issue).expect("Issue serialization is infallible");
obj["filename"] = Value::String(issue.filename.clone());
obj["body"] = Value::String(issue.body.clone());
obj["related_issues"] = json!(issue.related_issues);
match crate::workflow::category_of(issue, resolution) {
Some(c) => {
obj["category"] = json!(c.key());
obj["category_label"] = json!(c.label());
}
None => {
obj["category"] = Value::Null;
obj["category_label"] = json!("(unmapped)");
}
}
obj
}
pub(super) fn create_issue_from_template(
config: &Config,
store: &Store,
template_id: &str,
scope: Option<&str>,
fields: &cmd_templates::CreateFields<'_>,
resolution: &crate::workflow::CategoryResolution,
) -> ToolOutcome {
let fresh = template_management_config().ok().map(|(config, _)| config);
let config: &Config = fresh.as_ref().unwrap_or(config);
let selection = match cmd_templates::selection_for(template_id, scope) {
Ok(selection) => selection,
Err(error) => {
return Ok(template_error_result(
crate::error::render_error(&error),
template_outcome_value(
"create",
"rejected",
None,
vec![json!({
"kind": "invalid-scope",
"field": "scope",
"message": error.to_string(),
})],
),
));
}
};
let resolved = resolve(config, &selection, &fields.to_input());
if !resolved.is_ok() {
let diagnostics: Vec<Value> = resolved
.diagnostics()
.iter()
.map(|diagnostic| {
json!({
"kind": diagnostic.kind().as_str(),
"field": diagnostic.field(),
"message": diagnostic.message(),
})
})
.collect();
return Ok(template_error_result(
crate::error::render_error(&cmd_templates::resolution_error(&resolved)),
template_outcome_value("create", "rejected", None, diagnostics),
));
}
let template_id = resolved.template_id().to_string();
let scope = resolved.scope().to_string();
let request = resolved
.into_request()
.expect("a successful resolution always carries a request");
match cmd_templates::apply_request(config, store, &request) {
Ok(issue) => {
let mut value = issue_value(&issue, resolution);
value["template"] = json!({ "id": template_id, "scope": scope });
Ok(success_result(format!("Created {}", issue.filename), value))
}
Err(e) => Ok(error_result(crate::error::render_error(&e))),
}
}
pub(super) fn registry_config(store: &Store) -> TixResult<(Config, std::path::PathBuf)> {
let path = crate::commands::views::config_path(store);
let root = path.parent().map_or_else(
|| std::path::PathBuf::from("."),
std::path::Path::to_path_buf,
);
let config = Config::load(&root)?;
Ok((config, path))
}
pub(super) fn store_with_config(store: &Store, config: Config) -> Store {
Store {
issues_dir: store.issues_dir.clone(),
config,
}
}
pub(super) fn save_registry_config(config: &mut Config, path: &Path) -> TixResult<()> {
config.validate()?;
config.save(path).map_err(|error| {
crate::error::TixError::Config(format!("could not save `{}`: {error}", path.display()))
})
}
pub(super) fn trimmed_optional(value: Option<String>) -> Option<String> {
value
.map(|text| text.trim().to_string())
.filter(|text| !text.is_empty())
}
pub(super) fn 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),
}
}
pub(super) fn date_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 member_value(member: &crate::projects::MemberRef) -> Value {
json!({ "issue": member.issue.number(), "display": member.display })
}
pub(super) fn missing_reference_value(missing: &crate::projects::MissingProjectReference) -> Value {
json!({
"issue": missing.issue.number(),
"issue_display": missing.issue_display,
"project_id": missing.project_id,
"reason": missing.reason,
})
}
pub(super) fn project_record_value(project: &crate::projects::Project) -> Value {
json!({
"id": project.id.as_str(),
"name": project.name,
"description": project.description,
"status": project.status,
"lead": project.lead,
"start_date": project.start_date,
"due_date": project.due_date,
"member_count": project.member_count,
})
}
pub(super) fn project_value(project: &crate::projects::Project) -> Value {
let mut value = project_record_value(project);
value["members"] = json!(
project
.members
.iter()
.map(member_value)
.collect::<Vec<Value>>()
);
value
}
pub(super) fn project_health_value(report: &crate::projects::ProjectHealthReport) -> Value {
json!({
"project_id": report.project_id.as_str(),
"name": report.name,
"done": report.progress.done,
"total": report.progress.total,
"overdue": report.health.overdue,
"blocked": report.health.blocked,
"unassigned": report.health.unassigned,
"missing_references": report
.health
.missing_references
.iter()
.map(missing_reference_value)
.collect::<Vec<Value>>(),
})
}
pub(super) fn project_rows_text(projects: &[crate::projects::Project]) -> String {
projects
.iter()
.map(|project| format!("{}\t{}\t{}", project.id, project.name, project.member_count))
.collect::<Vec<String>>()
.join("\n")
}
pub(super) fn project_record_text(
project: &crate::projects::Project,
report: Option<&crate::projects::ProjectHealthReport>,
) -> String {
crate::commands::project::record_lines(project, report).join("\n")
}
pub(super) fn project_health_text(report: &crate::projects::ProjectHealthReport) -> String {
format!(
"{} {}/{}{}",
crate::projects::project_display(&report.project_id, &report.name),
report.progress.done,
report.progress.total,
crate::commands::view::health_suffix(report),
)
}
pub(super) fn deletion_notice(deletion: &crate::projects::ProjectDeletion) -> String {
if deletion.affected.is_empty() {
return String::new();
}
let plural = if deletion.affected.len() == 1 {
"issue"
} else {
"issues"
};
let mut out = format!(
"Note: {} {plural} still reference this project and keep the reference as a \
missing project reference.\nThey are not deleted or modified; `tix lint` reports \
the dangling reference until it is repaired.\n",
deletion.affected.len()
);
for member in &deletion.affected {
out.push_str(&format!(" {}\n", member.display));
}
out
}
pub(super) fn association_outcome_value(outcome: &crate::storage::BulkAssociationOutcome) -> Value {
match &outcome.outcome {
Ok(applied) => json!({
"selector": outcome.selector,
"changed": applied.changed,
"project_ids": applied.project_ids,
"written": applied.written.as_ref().map(|issue| issue.filename.clone()),
}),
Err(error) => json!({
"selector": outcome.selector,
"error": crate::error::render_error(error),
}),
}
}