1use crate::EngineError;
2use dpm_model::Plan;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::{BTreeMap, BTreeSet};
6
7mod applicability;
8mod external;
9pub use applicability::ApplicabilityChange;
10mod independence;
11mod patch;
12pub(crate) use independence::refuse_own_relaxation;
13pub use independence::{EdgeRelaxation, RelaxedConstraint};
14pub(crate) use patch::authorize;
15pub(crate) use patch::canonical;
16pub use patch::{apply_plan_change, json_equivalent, patch, plan_change};
17mod policy;
18mod protection;
19pub use protection::protected_work;
20mod replacement;
21pub use replacement::AffectedWork;
22mod revert;
23pub use revert::propose_revert;
24
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27#[serde(deny_unknown_fields)]
28pub struct EntityChange {
29 pub collection: String,
32 pub id: Option<String>,
34 pub fields: Vec<String>,
36 pub before: Value,
38 pub after: Value,
40}
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
44pub struct ChangePreview {
45 pub base_revision: u64,
47 pub changes: Vec<EntityChange>,
49 pub affected_work: Vec<AffectedWork>,
51 #[serde(default, skip_serializing_if = "Vec::is_empty")]
53 pub applicability_changes: Vec<ApplicabilityChange>,
54}
55
56pub fn propose_change(current: &Plan, proposed: &Plan) -> Result<ChangePreview, EngineError> {
61 current.validate()?;
62 proposed.validate()?;
63 protection::validate(current, proposed)?;
64 let changes = differences(current, proposed)?;
65 Ok(ChangePreview {
66 base_revision: current.revision,
67 changes,
68 affected_work: replacement::affected_work(current, proposed),
69 applicability_changes: applicability::changes(current, proposed),
70 })
71}
72
73const KEYED_COLLECTIONS: [&str; 7] = [
75 "projects",
76 "assets",
77 "work_items",
78 "requirements",
79 "decisions",
80 "risks",
81 "external_references",
82];
83
84fn field<'a>(value: &'a Value, key: &str) -> &'a Value {
90 static ABSENT: Value = Value::Null;
91 value.get(key).unwrap_or(&ABSENT)
92}
93
94pub(crate) fn differences(
95 current: &Plan,
96 proposed: &Plan,
97) -> Result<Vec<EntityChange>, EngineError> {
98 let before = serde_json::to_value(current)?;
99 let after = serde_json::to_value(proposed)?;
100 let mut changes = Vec::new();
101 append(
102 &mut changes,
103 "workspace",
104 None,
105 field(&before, "workspace"),
106 field(&after, "workspace"),
107 );
108 for collection in KEYED_COLLECTIONS {
109 let (old, new) = (field(&before, collection), field(&after, collection));
110 let ids: BTreeSet<_> = current_keys(old).chain(current_keys(new)).collect();
111 for id in ids {
112 append(
113 &mut changes,
114 collection,
115 Some(id.clone()),
116 field(old, &id),
117 field(new, &id),
118 );
119 }
120 }
121 let before_edges = edges_by_id(current)?;
122 let after_edges = edges_by_id(proposed)?;
123 for id in before_edges
124 .keys()
125 .chain(after_edges.keys())
126 .collect::<BTreeSet<_>>()
127 {
128 append(
129 &mut changes,
130 "dependencies",
131 Some(id.clone()),
132 before_edges.get(id).unwrap_or(&Value::Null),
133 after_edges.get(id).unwrap_or(&Value::Null),
134 );
135 }
136 append(
137 &mut changes,
138 "links",
139 None,
140 &sorted_links(current)?,
141 &sorted_links(proposed)?,
142 );
143 append(
144 &mut changes,
145 "calendars",
146 None,
147 field(&before, "calendars"),
148 field(&after, "calendars"),
149 );
150 Ok(changes)
151}
152
153fn current_keys(value: &Value) -> impl Iterator<Item = String> + '_ {
154 value
155 .as_object()
156 .into_iter()
157 .flat_map(|map| map.keys().cloned())
158}
159
160fn append(
161 changes: &mut Vec<EntityChange>,
162 collection: &str,
163 id: Option<String>,
164 before: &Value,
165 after: &Value,
166) {
167 if before == after {
168 return;
169 }
170 let fields = if before.is_object() && after.is_object() {
171 current_keys(before)
172 .chain(current_keys(after))
173 .collect::<BTreeSet<_>>()
174 .into_iter()
175 .filter(|field| before[field] != after[field])
176 .collect()
177 } else {
178 Vec::new()
179 };
180 changes.push(EntityChange {
181 collection: collection.into(),
182 id,
183 fields,
184 before: before.clone(),
185 after: after.clone(),
186 });
187}
188
189fn edges_by_id(plan: &Plan) -> Result<BTreeMap<String, Value>, EngineError> {
190 plan.dependencies
191 .iter()
192 .map(|edge| Ok((edge.id.to_string(), serde_json::to_value(edge)?)))
193 .collect()
194}
195
196fn sorted_links(plan: &Plan) -> Result<Value, EngineError> {
197 let mut links = plan.links.clone();
198 links.sort_by_key(|l| (l.kind, l.source, l.target));
199 Ok(serde_json::to_value(links)?)
200}
201
202pub(crate) fn invalid(entity: impl ToString, reason: &str) -> EngineError {
203 EngineError::InvalidCommand {
204 entity: entity.to_string(),
205 reason: reason.into(),
206 }
207}
208
209#[cfg(test)]
210mod tests;