use super::ImportResult;
use super::report::ItemReport;
use dpm_model::{Dependency, Key, Plan, WorkItemId};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceChange {
pub uid: i64,
pub guid: Option<String>,
pub key: Key,
pub attempted: Vec<String>,
}
impl fmt::Display for SourceChange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"MSPDI task UID {} (GUID {}) maps to {}",
self.uid,
self.guid.as_deref().unwrap_or("none"),
self.key
)?;
if self.attempted.is_empty() {
write!(
f,
" and changes none of its fields or incoming dependencies"
)
} else {
write!(f, " and would change {}", self.attempted.join("; "))
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LinkEndpoint {
pub uid: i64,
pub guid: Option<String>,
pub key: Key,
}
impl fmt::Display for LinkEndpoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"task UID {} (GUID {}, {})",
self.uid,
self.guid.as_deref().unwrap_or("none"),
self.key
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceLinkChange {
pub predecessor: LinkEndpoint,
pub successor: LinkEndpoint,
pub in_source: bool,
pub attempted: String,
}
impl fmt::Display for SourceLinkChange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.in_source {
write!(
f,
"MSPDI link from {} to {} would {}",
self.predecessor, self.successor, self.attempted
)
} else {
write!(
f,
"MSPDI document has no link from {} to {}, so the import would {}",
self.predecessor, self.successor, self.attempted
)
}
}
}
impl ImportResult {
#[must_use]
pub fn source_change(&self, current: &Plan, key: &str) -> Option<SourceChange> {
let item = self
.report
.items
.iter()
.find(|i| i.work.as_ref().is_some_and(|w| w.key.0 == key))?;
let id = item.work.as_ref()?.id;
let mut attempted: Vec<String> = item
.changes
.iter()
.map(|c| format!("{} {:?} -> {:?}", c.field, c.before, c.after))
.collect();
attempted.extend(edge_changes(current, &self.candidate, id));
Some(SourceChange {
uid: item.uid,
guid: item.guid.clone(),
key: item.work.as_ref()?.key.clone(),
attempted,
})
}
#[must_use]
pub fn source_link_change(&self, current: &Plan, dependency: &str) -> Option<SourceLinkChange> {
let local = current
.dependencies
.iter()
.find(|d| d.id.to_string() == dependency)?;
let describe = |d: &Dependency| describe(current, &self.candidate, d);
let Some(after) = self.candidate.find_dependency(local.id) else {
let by_work =
|id: WorkItemId| self.endpoint(|i| i.work.as_ref().is_some_and(|w| w.id == id));
return Some(SourceLinkChange {
predecessor: by_work(local.predecessor)?,
successor: by_work(local.successor)?,
in_source: false,
attempted: format!("remove dependency {}", describe(local)),
});
};
let link = self
.report
.links
.iter()
.find(|l| l.dependencies.contains(&local.id))?;
let predecessor_uid = link.predecessor_uid?;
let attempted = if local == after {
format!("keep dependency {} unchanged", describe(local))
} else {
format!(
"change dependency {} to {}",
describe(local),
describe(after)
)
};
Some(SourceLinkChange {
predecessor: self.endpoint(|i| i.uid == predecessor_uid)?,
successor: self.endpoint(|i| i.uid == link.successor_uid)?,
in_source: true,
attempted,
})
}
fn endpoint(&self, matches: impl Fn(&ItemReport) -> bool) -> Option<LinkEndpoint> {
let item = self.report.items.iter().find(|i| matches(i))?;
Some(LinkEndpoint {
uid: item.uid,
guid: item.guid.clone(),
key: item.work.as_ref()?.key.clone(),
})
}
}
fn describe(current: &Plan, candidate: &Plan, d: &Dependency) -> String {
let key = |id: WorkItemId| {
candidate
.work_items
.get(&id)
.or_else(|| current.work_items.get(&id))
.map_or_else(|| id.to_string(), |w| w.key.0.clone())
};
format!(
"{} {} -> {} lag {} h",
d.kind.abbreviation(),
key(d.predecessor),
key(d.successor),
d.lag_hours
)
}
fn edge_changes(current: &Plan, candidate: &Plan, id: WorkItemId) -> Vec<String> {
let into = |plan: &'_ Plan| -> Vec<Dependency> {
plan.dependencies
.iter()
.filter(|d| d.successor == id)
.cloned()
.collect()
};
let describe = |d: &Dependency| describe(current, candidate, d);
let (before, after) = (into(current), into(candidate));
let mut found = Vec::new();
for edge in &after {
match before.iter().find(|d| d.id == edge.id) {
None => found.push(format!("add dependency {}", describe(edge))),
Some(local) if local != edge => found.push(format!(
"change dependency {} to {}",
describe(local),
describe(edge)
)),
Some(_) => {}
}
}
for edge in before
.iter()
.filter(|d| !after.iter().any(|a| a.id == d.id))
{
found.push(format!("remove dependency {}", describe(edge)));
}
found
}