Skip to main content

harn_vm/triggers/
flow_control.rs

1use std::rc::Rc;
2use std::time::Duration;
3
4use crate::value::VmClosure;
5
6#[derive(Clone)]
7pub struct TriggerExpressionSpec {
8    pub raw: String,
9    pub closure: Rc<VmClosure>,
10}
11
12impl std::fmt::Debug for TriggerExpressionSpec {
13    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14        f.debug_struct("TriggerExpressionSpec")
15            .field("raw", &self.raw)
16            .finish()
17    }
18}
19
20#[derive(Clone, Debug)]
21pub struct TriggerConcurrencyConfig {
22    pub key: Option<TriggerExpressionSpec>,
23    pub max: u32,
24}
25
26#[derive(Clone, Debug)]
27pub struct TriggerThrottleConfig {
28    pub key: Option<TriggerExpressionSpec>,
29    pub period: Duration,
30    pub max: u32,
31}
32
33#[derive(Clone, Debug)]
34pub struct TriggerRateLimitConfig {
35    pub key: Option<TriggerExpressionSpec>,
36    pub period: Duration,
37    pub max: u32,
38}
39
40#[derive(Clone, Debug)]
41pub struct TriggerDebounceConfig {
42    pub key: TriggerExpressionSpec,
43    pub period: Duration,
44}
45
46#[derive(Clone, Debug)]
47pub struct TriggerSingletonConfig {
48    pub key: Option<TriggerExpressionSpec>,
49}
50
51#[derive(Clone, Debug)]
52pub struct TriggerBatchConfig {
53    pub key: Option<TriggerExpressionSpec>,
54    pub size: u32,
55    pub timeout: Duration,
56}
57
58#[derive(Clone, Debug)]
59pub struct TriggerPriorityOrderConfig {
60    pub key: TriggerExpressionSpec,
61    pub order: Vec<String>,
62}
63
64#[derive(Clone, Debug, Default)]
65pub struct TriggerFlowControlConfig {
66    pub concurrency: Option<TriggerConcurrencyConfig>,
67    pub throttle: Option<TriggerThrottleConfig>,
68    pub rate_limit: Option<TriggerRateLimitConfig>,
69    pub debounce: Option<TriggerDebounceConfig>,
70    pub singleton: Option<TriggerSingletonConfig>,
71    pub batch: Option<TriggerBatchConfig>,
72    pub priority: Option<TriggerPriorityOrderConfig>,
73}
74
75pub fn parse_flow_control_duration(raw: &str) -> Result<Duration, String> {
76    let trimmed = raw.trim();
77    if trimmed.len() < 2 {
78        return Err(format!("invalid duration '{raw}': expected <int><unit>"));
79    }
80    let split = trimmed
81        .find(|ch: char| !ch.is_ascii_digit())
82        .ok_or_else(|| format!("invalid duration '{raw}': missing unit suffix"))?;
83    if split == 0 || split == trimmed.len() {
84        return Err(format!("invalid duration '{raw}': expected <int><unit>"));
85    }
86    let value = trimmed[..split]
87        .parse::<u64>()
88        .map_err(|_| format!("invalid duration '{raw}': expected integer prefix"))?;
89    if value == 0 {
90        return Err(format!(
91            "invalid duration '{raw}': duration must be positive"
92        ));
93    }
94    let factor = match trimmed[split..].to_ascii_lowercase().as_str() {
95        "s" => 1,
96        "m" => 60,
97        "h" => 60 * 60,
98        "d" => 60 * 60 * 24,
99        "w" => 60 * 60 * 24 * 7,
100        other => {
101            return Err(format!(
102                "invalid duration '{raw}': unsupported unit '{other}', expected s/m/h/d/w"
103            ))
104        }
105    };
106    Ok(Duration::from_secs(value.saturating_mul(factor)))
107}