Skip to main content

turnframe_core/plan/
limits.rs

1//! Size limits on what a turn may be understood to ask.
2//!
3//! An understanding over a limit is refused whole, with the count and the bound, never
4//! trimmed to fit. Every limit is off by default: how much one message may ask for is
5//! the deployment's call.
6
7use serde::{Deserialize, Serialize};
8
9use crate::understanding::Understanding;
10
11/// Which limit was exceeded.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum PlanLimitKind {
15    /// Too many acts.
16    Acts,
17    /// Too many questions.
18    Questions,
19    /// Too many constraints.
20    Constraints,
21}
22
23/// An understanding exceeded a limit.
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
25#[error("plan exceeds {kind:?} limit: {actual} > {limit}")]
26pub struct PlanLimitError {
27    /// Which limit.
28    pub kind: PlanLimitKind,
29    /// The configured limit.
30    pub limit: usize,
31    /// The observed count.
32    pub actual: usize,
33}
34
35/// Limits applied to an [`Understanding`]; `None` is unlimited.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
37#[non_exhaustive]
38pub struct PlanLimits {
39    /// Maximum number of acts.
40    pub max_acts: Option<usize>,
41    /// Maximum number of questions.
42    pub max_questions: Option<usize>,
43    /// Maximum number of constraints.
44    pub max_constraints: Option<usize>,
45}
46
47impl PlanLimits {
48    /// No limits at all, which is what ships.
49    #[must_use]
50    pub const fn conservative() -> Self {
51        Self {
52            max_acts: None,
53            max_questions: None,
54            max_constraints: None,
55        }
56    }
57
58    /// Returns a copy with another act limit.
59    #[must_use]
60    pub const fn with_max_acts(mut self, max_acts: Option<usize>) -> Self {
61        self.max_acts = max_acts;
62        self
63    }
64
65    /// Returns a copy with another question limit.
66    #[must_use]
67    pub const fn with_max_questions(mut self, max_questions: Option<usize>) -> Self {
68        self.max_questions = max_questions;
69        self
70    }
71
72    /// Returns a copy with another constraint limit.
73    #[must_use]
74    pub const fn with_max_constraints(mut self, max_constraints: Option<usize>) -> Self {
75        self.max_constraints = max_constraints;
76        self
77    }
78
79    /// Checks `understanding` against the limits.
80    ///
81    /// # Errors
82    ///
83    /// The first limit exceeded.
84    pub fn enforce(&self, understanding: &Understanding) -> Result<(), PlanLimitError> {
85        check(PlanLimitKind::Acts, self.max_acts, understanding.acts.len())?;
86        check(
87            PlanLimitKind::Questions,
88            self.max_questions,
89            understanding.questions.len(),
90        )?;
91        check(
92            PlanLimitKind::Constraints,
93            self.max_constraints,
94            understanding.constraints.len(),
95        )
96    }
97}
98
99fn check(kind: PlanLimitKind, limit: Option<usize>, actual: usize) -> Result<(), PlanLimitError> {
100    match limit {
101        Some(limit) if actual > limit => Err(PlanLimitError {
102            kind,
103            limit,
104            actual,
105        }),
106        _ => Ok(()),
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::understanding::{
114        ActAction, ActId, ActStatus, ActTarget, UnderstoodAct, UnitId, WordRange,
115    };
116
117    fn act(unit: u16) -> UnderstoodAct {
118        UnderstoodAct {
119            id: ActId::new(UnitId(unit), 1),
120            action: ActAction::Start {
121                workflow: "w".into(),
122            },
123            target: ActTarget::New {
124                workflow: "w".into(),
125            },
126            arguments: std::collections::BTreeMap::new(),
127            words: WordRange {
128                first: 0,
129                last: 0,
130                start: 0,
131                end: 1,
132            },
133            depends_on: Vec::new(),
134            status: ActStatus::Ready,
135        }
136    }
137
138    #[test]
139    fn limits_are_enforced_with_the_count_and_the_bound() {
140        let limits = PlanLimits::default().with_max_acts(Some(1));
141        let understanding = Understanding {
142            acts: vec![act(1), act(2)],
143            ..Understanding::default()
144        };
145        let error = limits.enforce(&understanding).unwrap_err();
146        assert_eq!(error.kind, PlanLimitKind::Acts);
147        assert_eq!((error.limit, error.actual), (1, 2));
148        assert!(PlanLimits::default().enforce(&understanding).is_ok());
149    }
150}