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