Skip to main content

dpm_model/
condition.rs

1use crate::DecisionId;
2use serde::{Deserialize, Serialize};
3
4/// One structured alternative of a decision; `decide` must select exactly one option key.
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(deny_unknown_fields)]
7pub struct DecisionOption {
8    /// Stable option identifier, unique within the decision; the decided outcome names it.
9    pub key: String,
10    /// Human-readable description of the alternative.
11    pub label: String,
12}
13
14/// Applicability of work, and of everything it contains, to one option of a decision.
15///
16/// Conditions inherit through work packages: work applies only when every condition on it and on
17/// its containing packages names the selected option. Applicability is derived, never stored.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(deny_unknown_fields)]
20pub struct WorkCondition {
21    /// Decision whose selected option decides applicability; replacements are followed.
22    pub decision: DecisionId,
23    /// Option key that must be selected for the work to apply.
24    pub option: String,
25}
26
27/// How a merge point treats incoming constraints whose predecessor is not selected.
28///
29/// The default keeps ordinary dependency semantics: a constraint from work that a choice excluded
30/// never releases, so unrelated downstream work is not released by a skipped predecessor. Only an
31/// explicit branch join treats a skipped predecessor as an absent branch.
32#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case", tag = "mode")]
34pub enum JoinPolicy {
35    /// Every unwaived incoming constraint must be released.
36    #[default]
37    AllPredecessors,
38    /// Constraints from not-selected predecessors are absent branches; every other constraint must
39    /// be released, and at least one must remain unless `allow_empty` is set.
40    ActiveBranches {
41        /// Whether the join may complete when every branch was excluded by a choice.
42        #[serde(default)]
43        allow_empty: bool,
44    },
45}
46
47impl JoinPolicy {
48    /// Whether this is the default policy, which serialization omits.
49    #[must_use]
50    pub fn is_default(&self) -> bool {
51        *self == Self::AllPredecessors
52    }
53
54    /// Whether constraints from not-selected predecessors are skipped branches.
55    #[must_use]
56    pub fn skips_unselected(self) -> bool {
57        matches!(self, Self::ActiveBranches { .. })
58    }
59}
60
61impl crate::Decision {
62    /// Selected option key of a decided choice with structured options.
63    #[must_use]
64    pub fn selected_option(&self) -> Option<&str> {
65        if self.status != crate::DecisionStatus::Decided || self.options.is_empty() {
66            return None;
67        }
68        self.outcome.as_deref()
69    }
70
71    /// Whether `key` names one of this decision's options.
72    #[must_use]
73    pub fn has_option(&self, key: &str) -> bool {
74        self.options.iter().any(|o| o.key == key)
75    }
76}