Skip to main content

lastid_sdk/config/
polling.rs

1//! Polling configuration for credential requests.
2
3#[cfg(feature = "json-schema")]
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7use crate::error::LastIDError;
8
9/// Configuration for credential request polling behavior.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
12#[serde(default)]
13pub struct PollingConfig {
14    /// Initial polling interval (milliseconds)
15    #[serde(default = "default_initial_interval")]
16    pub initial_interval_ms: u64,
17
18    /// Maximum polling interval (milliseconds)
19    #[serde(default = "default_max_interval")]
20    pub max_interval_ms: u64,
21
22    /// Backoff multiplier (1.0 = no backoff)
23    #[serde(default = "default_backoff")]
24    pub backoff_multiplier: f64,
25
26    /// Maximum polling duration (seconds)
27    #[serde(default = "default_max_duration")]
28    pub max_duration_seconds: u64,
29}
30
31const fn default_initial_interval() -> u64 {
32    2000
33}
34const fn default_max_interval() -> u64 {
35    30000
36}
37const fn default_backoff() -> f64 {
38    1.5
39}
40const fn default_max_duration() -> u64 {
41    300
42}
43
44impl Default for PollingConfig {
45    fn default() -> Self {
46        Self {
47            initial_interval_ms: default_initial_interval(),
48            max_interval_ms: default_max_interval(),
49            backoff_multiplier: default_backoff(),
50            max_duration_seconds: default_max_duration(),
51        }
52    }
53}
54
55impl PollingConfig {
56    /// Validate the polling configuration.
57    pub fn validate(&self) -> Result<(), LastIDError> {
58        if self.initial_interval_ms == 0 {
59            return Err(LastIDError::config("initial_interval_ms must be > 0"));
60        }
61
62        if self.max_interval_ms < self.initial_interval_ms {
63            return Err(LastIDError::config(
64                "max_interval_ms must be >= initial_interval_ms",
65            ));
66        }
67
68        if self.backoff_multiplier < 1.0 {
69            return Err(LastIDError::config("backoff_multiplier must be >= 1.0"));
70        }
71
72        if self.max_duration_seconds == 0 {
73            return Err(LastIDError::config("max_duration_seconds must be > 0"));
74        }
75
76        Ok(())
77    }
78
79    /// Calculate the next polling interval with backoff.
80    #[must_use]
81    #[allow(
82        clippy::cast_precision_loss,
83        clippy::cast_possible_truncation,
84        clippy::cast_sign_loss
85    )]
86    pub fn next_interval(&self, current_ms: u64) -> u64 {
87        // Precision loss is acceptable for backoff calculations.
88        // Truncation is safe because multiplier >= 1.0 and we cap at max_interval.
89        // Sign loss impossible because both values are positive.
90        let next = (current_ms as f64 * self.backoff_multiplier) as u64;
91        next.min(self.max_interval_ms)
92    }
93
94    /// Merge another configuration into this one.
95    pub fn merge(&mut self, other: &Self) {
96        if other.initial_interval_ms != default_initial_interval() {
97            self.initial_interval_ms = other.initial_interval_ms;
98        }
99        if other.max_interval_ms != default_max_interval() {
100            self.max_interval_ms = other.max_interval_ms;
101        }
102        if (other.backoff_multiplier - default_backoff()).abs() > f64::EPSILON {
103            self.backoff_multiplier = other.backoff_multiplier;
104        }
105        if other.max_duration_seconds != default_max_duration() {
106            self.max_duration_seconds = other.max_duration_seconds;
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_default_values() {
117        let config = PollingConfig::default();
118        assert_eq!(config.initial_interval_ms, 2000);
119        assert_eq!(config.max_interval_ms, 30000);
120        assert!((config.backoff_multiplier - 1.5).abs() < f64::EPSILON);
121        assert_eq!(config.max_duration_seconds, 300);
122    }
123
124    #[test]
125    fn test_next_interval() {
126        let config = PollingConfig::default();
127
128        // First backoff: 2000 * 1.5 = 3000
129        assert_eq!(config.next_interval(2000), 3000);
130
131        // Second backoff: 3000 * 1.5 = 4500
132        assert_eq!(config.next_interval(3000), 4500);
133
134        // Capped at max: 30000 * 1.5 = 45000 -> capped to 30000
135        assert_eq!(config.next_interval(30000), 30000);
136    }
137
138    #[test]
139    fn test_validate_invalid() {
140        let mut config = PollingConfig::default();
141
142        config.initial_interval_ms = 0;
143        assert!(config.validate().is_err());
144
145        config.initial_interval_ms = 2000;
146        config.max_interval_ms = 1000; // Less than initial
147        assert!(config.validate().is_err());
148
149        config.max_interval_ms = 30000;
150        config.backoff_multiplier = 0.5; // Less than 1.0
151        assert!(config.validate().is_err());
152
153        config.backoff_multiplier = 1.5;
154        config.max_duration_seconds = 0;
155        assert!(config.validate().is_err());
156    }
157
158    #[test]
159    fn test_validate_valid() {
160        let config = PollingConfig::default();
161        assert!(config.validate().is_ok());
162    }
163}