use std::path::PathBuf;
use crate::domain::model::entity_ref::KnownRefs;
use crate::domain::model::entry_locator::EntryLocator;
use crate::domain::model::status::StatusesConfig;
use crate::domain::model::tag_descriptor::TagDescriptors;
use crate::domain::usecases::check::{dr_rules, issue_rules, DrCheckCtx, IssueCheckCtx};
use crate::domain::usecases::decision_record::DecisionRecordRepository;
use crate::domain::usecases::edit::dr::commit_dr_edits;
use crate::domain::usecases::edit::issue::commit_issue_edits;
use crate::domain::usecases::issue::IssueRepository;
fn locator_to_path(loc: &EntryLocator) -> PathBuf {
let s = loc.as_str();
let bare = s.strip_prefix("file://").unwrap_or(s);
PathBuf::from(bare)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum FixMode {
Apply,
DryRun,
}
#[derive(Debug)]
pub struct FixItem {
pub rule_id: &'static str,
pub description: String,
pub applied: bool,
}
#[derive(Debug, Default)]
pub struct FixReport {
pub items: Vec<FixItem>,
}
pub struct FixDrSource<'a> {
pub kind: &'a str,
pub repo: &'a dyn DecisionRecordRepository,
pub tag_descriptors: &'a TagDescriptors,
}
pub struct FixIssueSource<'a> {
pub repo: &'a dyn IssueRepository,
pub statuses: &'a StatusesConfig,
pub tag_descriptors: &'a TagDescriptors,
}
pub fn run_fix(
issue: FixIssueSource<'_>,
dr_sources: &[FixDrSource<'_>],
known_refs: &KnownRefs,
mode: FixMode,
) -> anyhow::Result<FixReport> {
let apply = matches!(mode, FixMode::Apply);
let mut report = FixReport::default();
for src in dr_sources {
let records: Vec<(PathBuf, _)> = src
.repo
.list()?
.into_iter()
.map(|r| (locator_to_path(&r.location), r))
.collect();
let ctx = DrCheckCtx {
repo: src.repo,
records: &records,
known_refs,
tag_descriptors: src.tag_descriptors,
};
let mut edits = Vec::new();
for rule in dr_rules() {
for finding in rule.find(&ctx)? {
if let Some(edit) = finding.fix {
report.items.push(FixItem {
rule_id: rule.id(),
description: edit.describe(),
applied: apply,
});
edits.push(edit);
}
}
}
if apply && !edits.is_empty() {
commit_dr_edits(src.repo, edits)?;
}
}
let issues: Vec<(PathBuf, _)> = issue
.repo
.list()?
.into_iter()
.map(|i| (locator_to_path(&i.location), i))
.collect();
let ctx = IssueCheckCtx {
repo: issue.repo,
issues: &issues,
known_refs,
statuses: issue.statuses,
tag_descriptors: issue.tag_descriptors,
};
let mut edits = Vec::new();
for rule in issue_rules() {
for finding in rule.find(&ctx)? {
if let Some(edit) = finding.fix {
report.items.push(FixItem {
rule_id: rule.id(),
description: edit.describe(),
applied: apply,
});
edits.push(edit);
}
}
}
if apply && !edits.is_empty() {
commit_issue_edits(issue.repo, edits)?;
}
Ok(report)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::model::entity_ref::KnownRefs;
use crate::domain::model::status::StatusesConfig;
use crate::domain::model::tag_descriptor::TagDescriptors;
use crate::domain::usecases::issue::test_support::FakeIssueRepository;
#[test]
fn clean_workspace_yields_an_empty_report() {
let repo = FakeIssueRepository::new();
let statuses = StatusesConfig::default_issue();
let td = TagDescriptors::default();
let report = run_fix(
FixIssueSource {
repo: &repo,
statuses: &statuses,
tag_descriptors: &td,
},
&[],
&KnownRefs::new(),
FixMode::Apply,
)
.unwrap();
assert!(report.items.is_empty());
}
#[test]
fn dry_run_does_not_call_commit_when_no_fixable_findings() {
let repo = FakeIssueRepository::new();
let statuses = StatusesConfig::default_issue();
let td = TagDescriptors::default();
let report = run_fix(
FixIssueSource {
repo: &repo,
statuses: &statuses,
tag_descriptors: &td,
},
&[],
&KnownRefs::new(),
FixMode::DryRun,
)
.unwrap();
assert!(report.items.is_empty());
assert_eq!(repo.save_count(), 0);
}
}