Skip to main content

made_core/value_objects/
num_agents.rs

1//! [`NumAgents`] value object — number of agents participating in a deliberation.
2
3use std::fmt;
4use std::num::NonZeroU32;
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::DomainError;
9
10/// Upper bound for agents in a single deliberation. Chosen conservatively:
11/// larger councils quickly hit diminishing returns on proposal diversity
12/// while multiplying downstream cost.
13pub const MAX_NUM_AGENTS: u32 = 64;
14
15/// Number of agents participating in a deliberation. Must be at least
16/// one — a deliberation without agents has no meaning.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[serde(transparent)]
19pub struct NumAgents(NonZeroU32);
20
21impl NumAgents {
22    pub fn new(value: u32) -> Result<Self, DomainError> {
23        if value > MAX_NUM_AGENTS {
24            return Err(DomainError::OutOfRange {
25                field: "num_agents",
26                value: f64::from(value),
27                min: 1.0,
28                max: f64::from(MAX_NUM_AGENTS),
29            });
30        }
31        let non_zero = NonZeroU32::new(value).ok_or(DomainError::MustBeNonZero {
32            field: "num_agents",
33        })?;
34        Ok(Self(non_zero))
35    }
36
37    #[must_use]
38    pub fn get(self) -> u32 {
39        self.0.get()
40    }
41}
42
43impl fmt::Display for NumAgents {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(f, "{}", self.0.get())
46    }
47}
48
49impl TryFrom<u32> for NumAgents {
50    type Error = DomainError;
51    fn try_from(value: u32) -> Result<Self, Self::Error> {
52        Self::new(value)
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn one_is_minimum() {
62        assert_eq!(NumAgents::new(1).unwrap().get(), 1);
63    }
64
65    #[test]
66    fn zero_is_rejected() {
67        assert!(matches!(
68            NumAgents::new(0).unwrap_err(),
69            DomainError::MustBeNonZero {
70                field: "num_agents"
71            }
72        ));
73    }
74
75    #[test]
76    fn upper_bound_is_accepted() {
77        assert!(NumAgents::new(MAX_NUM_AGENTS).is_ok());
78    }
79
80    #[test]
81    fn above_upper_bound_is_rejected() {
82        assert!(matches!(
83            NumAgents::new(MAX_NUM_AGENTS + 1).unwrap_err(),
84            DomainError::OutOfRange {
85                field: "num_agents",
86                ..
87            }
88        ));
89    }
90
91    #[test]
92    fn serde_is_transparent() {
93        assert_eq!(
94            serde_json::to_string(&NumAgents::new(3).unwrap()).unwrap(),
95            "3"
96        );
97    }
98}