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}