git_internal/internal/object/
plan.rs1use std::fmt;
34
35use serde::{Deserialize, Serialize};
36use uuid::Uuid;
37
38use crate::{
39 errors::GitError,
40 hash::ObjectHash,
41 internal::object::{
42 ObjectTrait,
43 types::{ActorRef, Header, ObjectType},
44 },
45};
46
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
52#[serde(deny_unknown_fields)]
53pub struct PlanStep {
54 step_id: Uuid,
60 description: String,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 inputs: Option<serde_json::Value>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
67 checks: Option<serde_json::Value>,
68}
69
70impl PlanStep {
71 pub fn new(description: impl Into<String>) -> Self {
73 Self {
74 step_id: Uuid::now_v7(),
75 description: description.into(),
76 inputs: None,
77 checks: None,
78 }
79 }
80
81 pub fn step_id(&self) -> Uuid {
83 self.step_id
84 }
85
86 pub fn description(&self) -> &str {
88 &self.description
89 }
90
91 pub fn inputs(&self) -> Option<&serde_json::Value> {
93 self.inputs.as_ref()
94 }
95
96 pub fn checks(&self) -> Option<&serde_json::Value> {
98 self.checks.as_ref()
99 }
100
101 pub fn set_inputs(&mut self, inputs: Option<serde_json::Value>) {
103 self.inputs = inputs;
104 }
105
106 pub fn set_checks(&mut self, checks: Option<serde_json::Value>) {
108 self.checks = checks;
109 }
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
118#[serde(deny_unknown_fields)]
119pub struct Plan {
120 #[serde(flatten)]
123 header: Header,
124 intent: Uuid,
126 #[serde(default, skip_serializing_if = "Vec::is_empty")]
128 parents: Vec<Uuid>,
129 #[serde(default, skip_serializing_if = "Vec::is_empty")]
136 context_frames: Vec<Uuid>,
137 #[serde(default)]
139 steps: Vec<PlanStep>,
140}
141
142impl Plan {
143 pub fn new(created_by: ActorRef, intent: Uuid) -> Result<Self, String> {
145 Ok(Self {
146 header: Header::new(ObjectType::Plan, created_by)?,
147 intent,
148 parents: Vec::new(),
149 context_frames: Vec::new(),
150 steps: Vec::new(),
151 })
152 }
153
154 pub fn new_revision(&self, created_by: ActorRef) -> Result<Self, String> {
157 Self::new_revision_chain(created_by, &[self])
158 }
159
160 pub fn new_revision_from(created_by: ActorRef, parent: &Self) -> Result<Self, String> {
162 Self::new_revision_chain(created_by, &[parent])
163 }
164
165 pub fn new_revision_chain(created_by: ActorRef, parents: &[&Self]) -> Result<Self, String> {
169 let first_parent = parents
170 .first()
171 .ok_or_else(|| "plan revision chain requires at least one parent".to_string())?;
172 let mut plan = Self::new(created_by, first_parent.intent)?;
173 for parent in parents {
174 if parent.intent != first_parent.intent {
175 return Err(format!(
176 "plan parents must belong to the same intent: expected {}, got {}",
177 first_parent.intent, parent.intent
178 ));
179 }
180 plan.add_parent(parent.header.object_id());
181 }
182 Ok(plan)
183 }
184
185 pub fn header(&self) -> &Header {
187 &self.header
188 }
189
190 pub fn intent(&self) -> Uuid {
192 self.intent
193 }
194
195 pub fn parents(&self) -> &[Uuid] {
197 &self.parents
198 }
199
200 pub fn add_parent(&mut self, parent_id: Uuid) {
202 if parent_id == self.header.object_id() {
203 return;
204 }
205 if !self.parents.contains(&parent_id) {
206 self.parents.push(parent_id);
207 }
208 }
209
210 pub fn set_parents(&mut self, parents: Vec<Uuid>) {
212 self.parents = parents;
213 }
214
215 pub fn context_frames(&self) -> &[Uuid] {
217 &self.context_frames
218 }
219
220 pub fn set_context_frames(&mut self, context_frames: Vec<Uuid>) {
223 self.context_frames = context_frames;
224 }
225
226 pub fn steps(&self) -> &[PlanStep] {
228 &self.steps
229 }
230
231 pub fn add_step(&mut self, step: PlanStep) {
233 self.steps.push(step);
234 }
235}
236
237impl fmt::Display for Plan {
238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 write!(f, "Plan: {}", self.header.object_id())
240 }
241}
242
243impl ObjectTrait for Plan {
244 fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
245 where
246 Self: Sized,
247 {
248 serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
249 }
250
251 fn get_type(&self) -> ObjectType {
252 ObjectType::Plan
253 }
254
255 fn get_size(&self) -> usize {
256 match serde_json::to_vec(self) {
257 Ok(v) => v.len(),
258 Err(e) => {
259 tracing::warn!("failed to compute Plan size: {}", e);
260 0
261 }
262 }
263 }
264
265 fn to_data(&self) -> Result<Vec<u8>, GitError> {
266 serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use serde_json::json;
273
274 use super::*;
275
276 #[test]
284 fn test_plan_revision_graph() {
285 let actor = ActorRef::human("jackie").expect("actor");
286 let intent_id = Uuid::from_u128(0x10);
287 let plan_v1 = Plan::new(actor.clone(), intent_id).expect("plan");
288 let plan_v2 = plan_v1.new_revision(actor.clone()).expect("plan");
289 let plan_v2b = Plan::new_revision_from(actor.clone(), &plan_v1).expect("plan");
290 let plan_v3 = Plan::new_revision_chain(actor, &[&plan_v2, &plan_v2b]).expect("plan");
291
292 assert!(plan_v1.parents().is_empty());
293 assert_eq!(plan_v2.parents(), &[plan_v1.header().object_id()]);
294 assert_eq!(
295 plan_v3.parents(),
296 &[plan_v2.header().object_id(), plan_v2b.header().object_id()]
297 );
298 assert_eq!(plan_v3.intent(), intent_id);
299 }
300
301 #[test]
302 fn test_plan_add_parent_dedupes_and_ignores_self() {
303 let actor = ActorRef::human("jackie").expect("actor");
304 let mut plan = Plan::new(actor, Uuid::from_u128(0x11)).expect("plan");
305 let parent_a = Uuid::from_u128(0x41);
306 let parent_b = Uuid::from_u128(0x42);
307
308 plan.add_parent(parent_a);
309 plan.add_parent(parent_a);
310 plan.add_parent(parent_b);
311 plan.add_parent(plan.header().object_id());
312
313 assert_eq!(plan.parents(), &[parent_a, parent_b]);
314 }
315
316 #[test]
317 fn test_plan_revision_chain_rejects_mixed_intents() {
318 let actor = ActorRef::human("jackie").expect("actor");
319 let plan_a = Plan::new(actor.clone(), Uuid::from_u128(0x100)).expect("plan");
320 let plan_b = Plan::new(actor, Uuid::from_u128(0x200)).expect("plan");
321
322 let err = Plan::new_revision_chain(
323 ActorRef::human("jackie").expect("actor"),
324 &[&plan_a, &plan_b],
325 )
326 .expect_err("mixed intents should fail");
327
328 assert!(err.contains("same intent"));
329 }
330
331 #[test]
332 fn test_plan_context_frames() {
333 let actor = ActorRef::human("jackie").expect("actor");
334 let mut plan = Plan::new(actor, Uuid::from_u128(0x12)).expect("plan");
335 let frame_a = Uuid::from_u128(0x51);
336 let frame_b = Uuid::from_u128(0x52);
337
338 plan.set_context_frames(vec![frame_a, frame_b]);
339 assert_eq!(plan.context_frames(), &[frame_a, frame_b]);
340 }
341
342 #[test]
343 fn test_plan_step_serializes_description_field() {
344 let step = PlanStep::new("run tests");
345 let value = serde_json::to_value(&step).expect("serialize step");
346
347 assert_eq!(
348 value.get("description").and_then(|v| v.as_str()),
349 Some("run tests")
350 );
351 assert!(value.get("step_id").is_some());
352 }
353
354 #[test]
355 fn test_plan_step_deserializes_description_field() {
356 let step_id = Uuid::from_u128(0x501);
357 let step: PlanStep = serde_json::from_value(json!({
358 "step_id": step_id,
359 "description": "run tests"
360 }))
361 .expect("deserialize step");
362
363 assert_eq!(step.step_id(), step_id);
364 assert_eq!(step.description(), "run tests");
365 }
366}