1use super::ImportResult;
7use super::report::ItemReport;
8use dpm_model::{Dependency, Key, Plan, WorkItemId};
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct SourceChange {
15 pub uid: i64,
17 pub guid: Option<String>,
19 pub key: Key,
21 pub attempted: Vec<String>,
23}
24
25impl fmt::Display for SourceChange {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 write!(
28 f,
29 "MSPDI task UID {} (GUID {}) maps to {}",
30 self.uid,
31 self.guid.as_deref().unwrap_or("none"),
32 self.key
33 )?;
34 if self.attempted.is_empty() {
35 write!(
36 f,
37 " and changes none of its fields or incoming dependencies"
38 )
39 } else {
40 write!(f, " and would change {}", self.attempted.join("; "))
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct LinkEndpoint {
48 pub uid: i64,
50 pub guid: Option<String>,
52 pub key: Key,
54}
55
56impl fmt::Display for LinkEndpoint {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 write!(
59 f,
60 "task UID {} (GUID {}, {})",
61 self.uid,
62 self.guid.as_deref().unwrap_or("none"),
63 self.key
64 )
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct SourceLinkChange {
71 pub predecessor: LinkEndpoint,
73 pub successor: LinkEndpoint,
75 pub in_source: bool,
78 pub attempted: String,
80}
81
82impl fmt::Display for SourceLinkChange {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 if self.in_source {
85 write!(
86 f,
87 "MSPDI link from {} to {} would {}",
88 self.predecessor, self.successor, self.attempted
89 )
90 } else {
91 write!(
92 f,
93 "MSPDI document has no link from {} to {}, so the import would {}",
94 self.predecessor, self.successor, self.attempted
95 )
96 }
97 }
98}
99
100impl ImportResult {
101 #[must_use]
103 pub fn source_change(&self, current: &Plan, key: &str) -> Option<SourceChange> {
104 let item = self
105 .report
106 .items
107 .iter()
108 .find(|i| i.work.as_ref().is_some_and(|w| w.key.0 == key))?;
109 let id = item.work.as_ref()?.id;
110 let mut attempted: Vec<String> = item
111 .changes
112 .iter()
113 .map(|c| format!("{} {:?} -> {:?}", c.field, c.before, c.after))
114 .collect();
115 attempted.extend(edge_changes(current, &self.candidate, id));
116 Some(SourceChange {
117 uid: item.uid,
118 guid: item.guid.clone(),
119 key: item.work.as_ref()?.key.clone(),
120 attempted,
121 })
122 }
123
124 #[must_use]
127 pub fn source_link_change(&self, current: &Plan, dependency: &str) -> Option<SourceLinkChange> {
128 let local = current
129 .dependencies
130 .iter()
131 .find(|d| d.id.to_string() == dependency)?;
132 let describe = |d: &Dependency| describe(current, &self.candidate, d);
133 let Some(after) = self.candidate.find_dependency(local.id) else {
134 let by_work =
135 |id: WorkItemId| self.endpoint(|i| i.work.as_ref().is_some_and(|w| w.id == id));
136 return Some(SourceLinkChange {
137 predecessor: by_work(local.predecessor)?,
138 successor: by_work(local.successor)?,
139 in_source: false,
140 attempted: format!("remove dependency {}", describe(local)),
141 });
142 };
143 let link = self
144 .report
145 .links
146 .iter()
147 .find(|l| l.dependencies.contains(&local.id))?;
148 let predecessor_uid = link.predecessor_uid?;
149 let attempted = if local == after {
150 format!("keep dependency {} unchanged", describe(local))
151 } else {
152 format!(
153 "change dependency {} to {}",
154 describe(local),
155 describe(after)
156 )
157 };
158 Some(SourceLinkChange {
159 predecessor: self.endpoint(|i| i.uid == predecessor_uid)?,
160 successor: self.endpoint(|i| i.uid == link.successor_uid)?,
161 in_source: true,
162 attempted,
163 })
164 }
165
166 fn endpoint(&self, matches: impl Fn(&ItemReport) -> bool) -> Option<LinkEndpoint> {
167 let item = self.report.items.iter().find(|i| matches(i))?;
168 Some(LinkEndpoint {
169 uid: item.uid,
170 guid: item.guid.clone(),
171 key: item.work.as_ref()?.key.clone(),
172 })
173 }
174}
175
176fn describe(current: &Plan, candidate: &Plan, d: &Dependency) -> String {
178 let key = |id: WorkItemId| {
179 candidate
180 .work_items
181 .get(&id)
182 .or_else(|| current.work_items.get(&id))
183 .map_or_else(|| id.to_string(), |w| w.key.0.clone())
184 };
185 format!(
186 "{} {} -> {} lag {} h",
187 d.kind.abbreviation(),
188 key(d.predecessor),
189 key(d.successor),
190 d.lag_hours
191 )
192}
193
194fn edge_changes(current: &Plan, candidate: &Plan, id: WorkItemId) -> Vec<String> {
196 let into = |plan: &'_ Plan| -> Vec<Dependency> {
197 plan.dependencies
198 .iter()
199 .filter(|d| d.successor == id)
200 .cloned()
201 .collect()
202 };
203 let describe = |d: &Dependency| describe(current, candidate, d);
204 let (before, after) = (into(current), into(candidate));
205 let mut found = Vec::new();
206 for edge in &after {
207 match before.iter().find(|d| d.id == edge.id) {
208 None => found.push(format!("add dependency {}", describe(edge))),
209 Some(local) if local != edge => found.push(format!(
210 "change dependency {} to {}",
211 describe(local),
212 describe(edge)
213 )),
214 Some(_) => {}
215 }
216 }
217 for edge in before
218 .iter()
219 .filter(|d| !after.iter().any(|a| a.id == d.id))
220 {
221 found.push(format!("remove dependency {}", describe(edge)));
222 }
223 found
224}