use std::fmt::Write as _;
use std::path::Path;
use agent_migrate_core::{
AgentKind, BundleRelativePath, ChangeKind, Compatibility, ConflictPolicy, FileKind, SecretRef,
WritePlan,
};
use serde::Serialize;
use crate::AdapterError;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ResourceConversion {
pub source_path: BundleRelativePath,
pub kind: FileKind,
pub compatibility: Compatibility,
pub target_path: Option<BundleRelativePath>,
pub change: ChangeKind,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ReportSummary {
pub added: usize,
pub identical: usize,
pub merged: usize,
pub replaced: usize,
pub renamed: usize,
pub skipped: usize,
pub conflicts: usize,
pub warnings: usize,
pub blocked: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ConversionReport {
pub schema_version: &'static str,
pub source: AgentKind,
pub target: AgentKind,
pub resources: Vec<ResourceConversion>,
pub secret_references: Vec<SecretRef>,
pub summary: ReportSummary,
}
impl ConversionReport {
pub(crate) fn new(
source: AgentKind,
target: AgentKind,
resources: Vec<ResourceConversion>,
mut secret_references: Vec<SecretRef>,
) -> Self {
secret_references.sort_by(|left, right| {
left.logical_location
.cmp(&right.logical_location)
.then_with(|| left.name.cmp(&right.name))
});
let mut report = Self {
schema_version: "1.0",
source,
target,
resources,
secret_references,
summary: ReportSummary::default(),
};
report.recount();
report
}
pub fn classify_changes(
&mut self,
root: &Path,
plan: &WritePlan,
policy: ConflictPolicy,
) -> Result<(), AdapterError> {
for resource in &mut self.resources {
resource.change = classify(resource.target_path.as_ref(), root, plan, policy)?;
}
self.recount();
Ok(())
}
#[must_use]
pub fn to_markdown(&self) -> String {
let mut output = format!(
"# Agent Config Migrate Report\n\nSchema: {}\n\nSource: `{}` \nTarget: `{}`\n\n| Change | Kind | Source | Target | Compatibility |\n| --- | --- | --- | --- | --- |\n",
self.schema_version, self.source, self.target
);
for resource in &self.resources {
if writeln!(
output,
"| {:?} | {:?} | `{}` | `{}` | `{}` |",
resource.change,
resource.kind,
escape(&resource.source_path.to_string()),
resource
.target_path
.as_ref()
.map_or_else(|| "-".to_owned(), ToString::to_string),
compatibility_name(&resource.compatibility),
)
.is_err()
{
return output;
}
}
output.push_str("\n## Required Secrets\n\n");
for reference in &self.secret_references {
if writeln!(
output,
"- `{}` at `{}` (required: {})",
escape(&reference.name),
escape(&reference.logical_location),
reference.required
)
.is_err()
{
return output;
}
}
output
}
fn recount(&mut self) {
let mut summary = ReportSummary::default();
for resource in &self.resources {
match resource.change {
ChangeKind::Add => summary.added += 1,
ChangeKind::Identical => summary.identical += 1,
ChangeKind::Merge => summary.merged += 1,
ChangeKind::Replace => summary.replaced += 1,
ChangeKind::Rename => summary.renamed += 1,
ChangeKind::Skip => summary.skipped += 1,
ChangeKind::Conflict => summary.conflicts += 1,
}
match &resource.compatibility {
Compatibility::Lossy { .. } | Compatibility::PreservedUnavailable { .. } => {
summary.warnings += 1;
}
Compatibility::Blocked { .. } => summary.blocked += 1,
Compatibility::Exact | Compatibility::Converted | Compatibility::Bridged { .. } => {
}
}
}
self.summary = summary;
}
}
fn classify(
path: Option<&BundleRelativePath>,
root: &Path,
plan: &WritePlan,
policy: ConflictPolicy,
) -> Result<ChangeKind, AdapterError> {
let Some(path) = path else {
return Ok(ChangeKind::Skip);
};
let Some(write) = plan.get(path) else {
return Ok(ChangeKind::Skip);
};
let target = root.join(path.as_path());
if !target.exists() {
return Ok(ChangeKind::Add);
}
let existing = std::fs::read(&target).map_err(|source| AdapterError::Io {
path: target,
source,
})?;
if existing == write.content {
return Ok(ChangeKind::Identical);
}
Ok(match policy {
ConflictPolicy::Fail => ChangeKind::Conflict,
ConflictPolicy::Replace => ChangeKind::Replace,
ConflictPolicy::Merge => ChangeKind::Merge,
ConflictPolicy::Rename => ChangeKind::Rename,
})
}
fn compatibility_name(value: &Compatibility) -> &'static str {
match value {
Compatibility::Exact => "exact",
Compatibility::Converted => "converted",
Compatibility::Bridged { .. } => "bridged",
Compatibility::Lossy { .. } => "lossy",
Compatibility::PreservedUnavailable { .. } => "preserved_unavailable",
Compatibility::Blocked { .. } => "blocked",
}
}
fn escape(value: &str) -> String {
value.replace(['\n', '\r'], " ").replace('|', "\\|")
}