use std::fmt;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
errors::GitError,
hash::ObjectHash,
internal::object::{
ObjectTrait,
types::{ActorRef, Header, ObjectType},
},
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct PlanStep {
step_id: Uuid,
description: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
inputs: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
checks: Option<serde_json::Value>,
}
impl PlanStep {
pub fn new(description: impl Into<String>) -> Self {
Self {
step_id: Uuid::now_v7(),
description: description.into(),
inputs: None,
checks: None,
}
}
pub fn step_id(&self) -> Uuid {
self.step_id
}
pub fn description(&self) -> &str {
&self.description
}
pub fn inputs(&self) -> Option<&serde_json::Value> {
self.inputs.as_ref()
}
pub fn checks(&self) -> Option<&serde_json::Value> {
self.checks.as_ref()
}
pub fn set_inputs(&mut self, inputs: Option<serde_json::Value>) {
self.inputs = inputs;
}
pub fn set_checks(&mut self, checks: Option<serde_json::Value>) {
self.checks = checks;
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Plan {
#[serde(flatten)]
header: Header,
intent: Uuid,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
parents: Vec<Uuid>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
context_frames: Vec<Uuid>,
#[serde(default)]
steps: Vec<PlanStep>,
}
impl Plan {
pub fn new(created_by: ActorRef, intent: Uuid) -> Result<Self, String> {
Ok(Self {
header: Header::new(ObjectType::Plan, created_by)?,
intent,
parents: Vec::new(),
context_frames: Vec::new(),
steps: Vec::new(),
})
}
pub fn new_revision(&self, created_by: ActorRef) -> Result<Self, String> {
Self::new_revision_chain(created_by, &[self])
}
pub fn new_revision_from(created_by: ActorRef, parent: &Self) -> Result<Self, String> {
Self::new_revision_chain(created_by, &[parent])
}
pub fn new_revision_chain(created_by: ActorRef, parents: &[&Self]) -> Result<Self, String> {
let first_parent = parents
.first()
.ok_or_else(|| "plan revision chain requires at least one parent".to_string())?;
let mut plan = Self::new(created_by, first_parent.intent)?;
for parent in parents {
if parent.intent != first_parent.intent {
return Err(format!(
"plan parents must belong to the same intent: expected {}, got {}",
first_parent.intent, parent.intent
));
}
plan.add_parent(parent.header.object_id());
}
Ok(plan)
}
pub fn header(&self) -> &Header {
&self.header
}
pub fn intent(&self) -> Uuid {
self.intent
}
pub fn parents(&self) -> &[Uuid] {
&self.parents
}
pub fn add_parent(&mut self, parent_id: Uuid) {
if parent_id == self.header.object_id() {
return;
}
if !self.parents.contains(&parent_id) {
self.parents.push(parent_id);
}
}
pub fn set_parents(&mut self, parents: Vec<Uuid>) {
self.parents = parents;
}
pub fn context_frames(&self) -> &[Uuid] {
&self.context_frames
}
pub fn set_context_frames(&mut self, context_frames: Vec<Uuid>) {
self.context_frames = context_frames;
}
pub fn steps(&self) -> &[PlanStep] {
&self.steps
}
pub fn add_step(&mut self, step: PlanStep) {
self.steps.push(step);
}
}
impl fmt::Display for Plan {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Plan: {}", self.header.object_id())
}
}
impl ObjectTrait for Plan {
fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
where
Self: Sized,
{
serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
}
fn get_type(&self) -> ObjectType {
ObjectType::Plan
}
fn get_size(&self) -> usize {
match serde_json::to_vec(self) {
Ok(v) => v.len(),
Err(e) => {
tracing::warn!("failed to compute Plan size: {}", e);
0
}
}
}
fn to_data(&self) -> Result<Vec<u8>, GitError> {
serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn test_plan_revision_graph() {
let actor = ActorRef::human("jackie").expect("actor");
let intent_id = Uuid::from_u128(0x10);
let plan_v1 = Plan::new(actor.clone(), intent_id).expect("plan");
let plan_v2 = plan_v1.new_revision(actor.clone()).expect("plan");
let plan_v2b = Plan::new_revision_from(actor.clone(), &plan_v1).expect("plan");
let plan_v3 = Plan::new_revision_chain(actor, &[&plan_v2, &plan_v2b]).expect("plan");
assert!(plan_v1.parents().is_empty());
assert_eq!(plan_v2.parents(), &[plan_v1.header().object_id()]);
assert_eq!(
plan_v3.parents(),
&[plan_v2.header().object_id(), plan_v2b.header().object_id()]
);
assert_eq!(plan_v3.intent(), intent_id);
}
#[test]
fn test_plan_add_parent_dedupes_and_ignores_self() {
let actor = ActorRef::human("jackie").expect("actor");
let mut plan = Plan::new(actor, Uuid::from_u128(0x11)).expect("plan");
let parent_a = Uuid::from_u128(0x41);
let parent_b = Uuid::from_u128(0x42);
plan.add_parent(parent_a);
plan.add_parent(parent_a);
plan.add_parent(parent_b);
plan.add_parent(plan.header().object_id());
assert_eq!(plan.parents(), &[parent_a, parent_b]);
}
#[test]
fn test_plan_revision_chain_rejects_mixed_intents() {
let actor = ActorRef::human("jackie").expect("actor");
let plan_a = Plan::new(actor.clone(), Uuid::from_u128(0x100)).expect("plan");
let plan_b = Plan::new(actor, Uuid::from_u128(0x200)).expect("plan");
let err = Plan::new_revision_chain(
ActorRef::human("jackie").expect("actor"),
&[&plan_a, &plan_b],
)
.expect_err("mixed intents should fail");
assert!(err.contains("same intent"));
}
#[test]
fn test_plan_context_frames() {
let actor = ActorRef::human("jackie").expect("actor");
let mut plan = Plan::new(actor, Uuid::from_u128(0x12)).expect("plan");
let frame_a = Uuid::from_u128(0x51);
let frame_b = Uuid::from_u128(0x52);
plan.set_context_frames(vec![frame_a, frame_b]);
assert_eq!(plan.context_frames(), &[frame_a, frame_b]);
}
#[test]
fn test_plan_step_serializes_description_field() {
let step = PlanStep::new("run tests");
let value = serde_json::to_value(&step).expect("serialize step");
assert_eq!(
value.get("description").and_then(|v| v.as_str()),
Some("run tests")
);
assert!(value.get("step_id").is_some());
}
#[test]
fn test_plan_step_deserializes_description_field() {
let step_id = Uuid::from_u128(0x501);
let step: PlanStep = serde_json::from_value(json!({
"step_id": step_id,
"description": "run tests"
}))
.expect("deserialize step");
assert_eq!(step.step_id(), step_id);
assert_eq!(step.description(), "run tests");
}
}