1use serde::{Deserialize, Serialize};
11
12use crate::plan::{Deliverable, Estimate, PlanGraph, PlannerError, Prerequisite};
13use crate::planner::validate_graph;
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21#[serde(tag = "op", rename_all = "snake_case", deny_unknown_fields)]
22pub enum GraphEdit {
23 RemoveEdge { from: String, to: String },
25 AddEdge {
27 from: String,
28 to: String,
29 #[serde(default)]
30 consumes: Option<String>,
31 },
32 SetEffort { id: String, hours: f32 },
34 SetDuration { id: String, hours: Option<f32> },
36 SetEstimate {
38 id: String,
39 estimate: Option<Estimate>,
40 },
41 SetMetadata {
43 id: String,
44 key: String,
45 value: serde_json::Value,
46 },
47 RemoveDeliverable { id: String },
49 AddDeliverable { deliverable: Deliverable },
51}
52
53pub fn apply_edits(graph: &PlanGraph, edits: &[GraphEdit]) -> Result<PlanGraph, PlannerError> {
62 let mut out = graph.clone();
63 for (index, edit) in edits.iter().enumerate() {
64 apply_one(&mut out, index, edit)?;
65 }
66 validate_graph(&out).map_err(|err| match err {
67 PlannerError::InvalidGraph { reason } => PlannerError::InvalidGraph {
68 reason: format!("after applying {} edits: {reason}", edits.len()),
69 },
70 other => other,
71 })?;
72 Ok(out)
73}
74
75fn unknown_id(index: usize, id: &str) -> PlannerError {
76 PlannerError::InvalidGraph {
77 reason: format!("edit {index}: unknown deliverable id '{id}'"),
78 }
79}
80
81fn contains_id(graph: &PlanGraph, id: &str) -> bool {
82 graph.deliverables.iter().any(|d| d.id == id)
83}
84
85fn find_mut<'a>(graph: &'a mut PlanGraph, id: &str) -> Option<&'a mut Deliverable> {
86 graph.deliverables.iter_mut().find(|d| d.id == id)
87}
88
89fn apply_one(graph: &mut PlanGraph, index: usize, edit: &GraphEdit) -> Result<(), PlannerError> {
90 match edit {
91 GraphEdit::RemoveEdge { from, to } => {
92 if !contains_id(graph, to) {
93 return Err(unknown_id(index, to));
94 }
95 if !contains_id(graph, from) {
96 return Err(unknown_id(index, from));
97 }
98 let target = find_mut(graph, to).expect("checked present above");
99 let before = target.prerequisites.len();
100 target.prerequisites.retain(|p| p.id() != from);
101 if target.prerequisites.len() == before {
102 return Err(PlannerError::InvalidGraph {
103 reason: format!("edit {index}: no edge '{from}' -> '{to}'"),
104 });
105 }
106 }
107 GraphEdit::AddEdge { from, to, consumes } => {
108 if !contains_id(graph, to) {
109 return Err(unknown_id(index, to));
110 }
111 if !contains_id(graph, from) {
112 return Err(unknown_id(index, from));
113 }
114 let edge = match consumes {
115 Some(consumes) => Prerequisite::Edge {
116 id: from.clone(),
117 consumes: Some(consumes.clone()),
118 kind: None,
119 lag_hours: None,
120 },
121 None => Prerequisite::Id(from.clone()),
122 };
123 let target = find_mut(graph, to).expect("checked present above");
124 if target.prerequisites.iter().any(|p| p.id() == from) {
125 return Err(PlannerError::InvalidGraph {
126 reason: format!("edit {index}: edge '{from}' -> '{to}' already exists"),
127 });
128 }
129 target.prerequisites.push(edge);
130 }
131 GraphEdit::SetEffort { id, hours } => {
132 let target = find_mut(graph, id).ok_or_else(|| unknown_id(index, id))?;
133 target.estimated_effort_hours = Some(*hours);
134 }
135 GraphEdit::SetDuration { id, hours } => {
136 let target = find_mut(graph, id).ok_or_else(|| unknown_id(index, id))?;
137 target.duration_hours = *hours;
138 }
139 GraphEdit::SetEstimate { id, estimate } => {
140 let target = find_mut(graph, id).ok_or_else(|| unknown_id(index, id))?;
141 target.estimate = *estimate;
142 }
143 GraphEdit::SetMetadata { id, key, value } => {
144 let target = find_mut(graph, id).ok_or_else(|| unknown_id(index, id))?;
145 if !target.metadata.is_null() && !target.metadata.is_object() {
146 return Err(PlannerError::InvalidGraph {
147 reason: format!("edit {index}: metadata of '{id}' is not an object"),
148 });
149 }
150 if target.metadata.is_null() {
151 target.metadata = serde_json::json!({});
152 }
153 target
154 .metadata
155 .as_object_mut()
156 .expect("metadata coerced to an object above")
157 .insert(key.clone(), value.clone());
158 }
159 GraphEdit::RemoveDeliverable { id } => {
160 if !contains_id(graph, id) {
161 return Err(unknown_id(index, id));
162 }
163 let mut dependents: Vec<&str> = graph
164 .deliverables
165 .iter()
166 .filter(|d| d.id != *id && d.prerequisites.iter().any(|p| p.id() == id))
167 .map(|d| d.id.as_str())
168 .collect();
169 if !dependents.is_empty() {
170 dependents.sort_unstable();
171 return Err(PlannerError::InvalidGraph {
172 reason: format!(
173 "edit {index}: '{id}' is a prerequisite of [{}]; remove those edges first",
174 dependents.join(", ")
175 ),
176 });
177 }
178 graph.deliverables.retain(|d| d.id != *id);
179 }
180 GraphEdit::AddDeliverable { deliverable } => {
181 if contains_id(graph, &deliverable.id) {
182 return Err(PlannerError::InvalidGraph {
183 reason: format!(
184 "edit {index}: deliverable '{}' already exists",
185 deliverable.id
186 ),
187 });
188 }
189 for prereq in &deliverable.prerequisites {
190 if !contains_id(graph, prereq.id()) {
191 return Err(unknown_id(index, prereq.id()));
192 }
193 }
194 graph.deliverables.push(deliverable.clone());
195 }
196 }
197 Ok(())
198}