made-core 0.8.0

Domain core of MADE: entities, value objects, events, ports. No IO.
Documentation
use serde::{Deserialize, Serialize};

use super::{
    ChildrenCompletedCondition, JoinStepCount, OutputFieldGuardCondition, StepId,
    StepRepeatExhaustedGuardCondition, StepStatus,
};

mod counted_join_condition;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum GuardCondition {
    Always,
    AllStepsCompleted,
    AnyStepCompleted,
    StepsCompleted(#[serde(with = "counted_join_condition")] JoinStepCount),
    StepStatus { step_id: StepId, status: StepStatus },
    OutputField(OutputFieldGuardCondition),
    StepRepeatExhausted(StepRepeatExhaustedGuardCondition),
    ChildrenCompleted(ChildrenCompletedCondition),
    HumanApproval,
}

impl GuardCondition {
    #[must_use]
    pub fn referenced_step_id(&self) -> Option<&StepId> {
        match self {
            Self::StepStatus { step_id, .. } => Some(step_id),
            Self::OutputField(condition) => Some(condition.step_id()),
            Self::StepRepeatExhausted(condition) => Some(condition.step_id()),
            Self::ChildrenCompleted(condition) => Some(condition.step_id()),
            Self::Always
            | Self::AllStepsCompleted
            | Self::AnyStepCompleted
            | Self::StepsCompleted(_)
            | Self::HumanApproval => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn counted_join_serializes_as_tagged_object() {
        let condition = GuardCondition::StepsCompleted(JoinStepCount::new(2).unwrap());
        let encoded = serde_json::to_string(&condition).unwrap();
        assert_eq!(encoded, r#"{"kind":"steps_completed","count":2}"#);
        assert_eq!(
            serde_json::from_str::<GuardCondition>(&encoded).unwrap(),
            condition
        );
    }

    #[test]
    fn counted_join_deserialization_preserves_positive_count_invariant() {
        for encoded in [
            r#"{"kind":"steps_completed","count":0}"#,
            r#"{"kind":"steps_completed","count":-1}"#,
            r#"{"kind":"steps_completed"}"#,
            r#"{"kind":"steps_completed","count":4294967296}"#,
        ] {
            assert!(
                serde_json::from_str::<GuardCondition>(encoded).is_err(),
                "{encoded}"
            );
        }
    }

    #[test]
    fn other_guard_variants_keep_their_exact_canonical_bytes() {
        use crate::value_objects::StepOutputField;
        let step = StepId::new("inspect_api").unwrap();
        let cases = [
            (GuardCondition::Always, r#"{"kind":"always"}"#),
            (
                GuardCondition::AllStepsCompleted,
                r#"{"kind":"all_steps_completed"}"#,
            ),
            (
                GuardCondition::AnyStepCompleted,
                r#"{"kind":"any_step_completed"}"#,
            ),
            (
                GuardCondition::HumanApproval,
                r#"{"kind":"human_approval"}"#,
            ),
            (
                GuardCondition::StepStatus {
                    step_id: step.clone(),
                    status: StepStatus::Completed,
                },
                r#"{"kind":"step_status","step_id":"inspect_api","status":"COMPLETED"}"#,
            ),
            (
                GuardCondition::OutputField(OutputFieldGuardCondition::new(
                    step.clone(),
                    StepOutputField::new("approved").unwrap(),
                    serde_json::json!(true),
                )),
                r#"{"kind":"output_field","step_id":"inspect_api","output_field":"approved","expected":true}"#,
            ),
            (
                GuardCondition::StepRepeatExhausted(StepRepeatExhaustedGuardCondition::new(step)),
                r#"{"kind":"step_repeat_exhausted","step_id":"inspect_api"}"#,
            ),
        ];
        for (condition, encoded) in cases {
            assert_eq!(serde_json::to_string(&condition).unwrap(), encoded);
            assert_eq!(
                serde_json::from_str::<GuardCondition>(encoded).unwrap(),
                condition
            );
        }
    }
}