Skip to main content

turnframe_core/plan/
mod.rs

1//! What an operation declares about the plans it may appear in: its target, whether it
2//! mutates, and who may ask for it. The plan itself is an
3//! [`Understanding`](crate::understanding::Understanding).
4
5pub mod limits;
6
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10use crate::understanding::ActTarget;
11
12/// Which record an operation may be aimed at.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
14#[serde(rename_all = "snake_case")]
15pub enum TargetPolicy {
16    /// A record that exists: in view, created earlier this turn, or named by the user.
17    RequiresExistingCase,
18    /// A record the turn already has in view, or the one the card on screen is about.
19    RequiresCatalogedCase,
20    /// An existing record or a new one.
21    AllowsNewCase,
22    /// Only a new record: the operation opens one.
23    NewCaseOnly,
24    /// Only the record of the card on screen.
25    ActiveInteractionOnly,
26    /// No record at all.
27    None,
28}
29
30impl TargetPolicy {
31    /// Whether an act aimed at `target` is admissible under this policy.
32    #[must_use]
33    pub const fn permits(self, target: &ActTarget) -> bool {
34        match self {
35            Self::RequiresExistingCase => matches!(
36                target,
37                ActTarget::Record { .. }
38                    | ActTarget::SameTurn { .. }
39                    | ActTarget::NotListed { .. }
40                    | ActTarget::Ambiguous { .. }
41            ),
42            Self::RequiresCatalogedCase => matches!(
43                target,
44                ActTarget::Record { .. }
45                    | ActTarget::SameTurn { .. }
46                    | ActTarget::Ambiguous { .. }
47                    | ActTarget::Card
48            ),
49            Self::AllowsNewCase => !matches!(target, ActTarget::Card | ActTarget::Nothing),
50            Self::NewCaseOnly => matches!(target, ActTarget::New { .. }),
51            Self::ActiveInteractionOnly => matches!(target, ActTarget::Card),
52            Self::None => true,
53        }
54    }
55}
56
57/// Who may ask for an operation.
58///
59/// A card's option may run an operation a model must never propose: the confirm button
60/// of a recap. `CardOnly` keeps it off every task's closed set and admits it only from
61/// the card that carries it.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, JsonSchema)]
63#[serde(rename_all = "snake_case")]
64pub enum ActAvailability {
65    /// Understanding may route to it, and a card may carry it. The default.
66    #[default]
67    Proposable,
68    /// Only an action a card carries may run it.
69    CardOnly,
70}
71
72impl ActAvailability {
73    /// Whether understanding may route a request to it.
74    #[must_use]
75    pub const fn is_proposable(self) -> bool {
76        matches!(self, Self::Proposable)
77    }
78}
79
80/// Whether an operation changes a record.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
82#[serde(rename_all = "snake_case")]
83pub enum ActMutability {
84    /// Reads only.
85    ReadOnly,
86    /// Changes a record.
87    Mutating,
88}
89
90/// Which state a question is answered against. The server may override an unsafe basis.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
92#[serde(rename_all = "snake_case")]
93pub enum AnswerBasis {
94    /// What is committed now.
95    CurrentCommittedState,
96    /// What the turn proposes but has not committed.
97    ProposedState,
98    /// What will be committed after this turn's commands.
99    CommittedStateAfterTurn,
100    /// Domain knowledge independent of any record.
101    GeneralDomainKnowledge,
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::understanding::{ActId, UnitId};
108
109    #[test]
110    fn a_policy_admits_exactly_its_targets() {
111        let record = ActTarget::Record { token: "t".into() };
112        let same_turn = ActTarget::SameTurn {
113            act: ActId::new(UnitId(1), 1),
114        };
115        let new = ActTarget::New {
116            workflow: "w".into(),
117        };
118        assert!(TargetPolicy::RequiresExistingCase.permits(&record));
119        assert!(TargetPolicy::RequiresExistingCase.permits(&same_turn));
120        assert!(!TargetPolicy::RequiresExistingCase.permits(&new));
121        assert!(
122            !TargetPolicy::RequiresCatalogedCase.permits(&ActTarget::NotListed {
123                workflow: "w".into(),
124                words: None,
125            })
126        );
127        assert!(TargetPolicy::NewCaseOnly.permits(&new));
128        assert!(!TargetPolicy::NewCaseOnly.permits(&record));
129        assert!(TargetPolicy::ActiveInteractionOnly.permits(&ActTarget::Card));
130        assert!(!TargetPolicy::AllowsNewCase.permits(&ActTarget::Card));
131    }
132}