cronback_api_model/
trigger.rs

1use chrono::{DateTime, Utc};
2#[cfg(feature = "dto")]
3use dto::{FromProto, IntoProto};
4use serde::{Deserialize, Serialize};
5use serde_with::skip_serializing_none;
6use strum::Display;
7#[cfg(feature = "validation")]
8use validator::Validate;
9
10use super::{Action, Payload, Schedule};
11use crate::{Recurring, RunAt, Webhook};
12
13#[derive(Debug, Deserialize, Default)]
14#[cfg_attr(feature = "validation", derive(Validate))]
15#[cfg_attr(
16    feature = "dto",
17    derive(IntoProto),
18    proto(target = "proto::scheduler_proto::ListTriggersFilter")
19)]
20pub struct TriggersFilter {
21    #[serde(default)]
22    #[cfg_attr(feature = "dto", proto(name = "statuses"))]
23    pub status: Vec<TriggerStatus>,
24}
25
26#[derive(Debug, Display, Clone, Copy, Serialize, Deserialize, PartialEq)]
27#[cfg_attr(feature = "client", non_exhaustive)]
28#[cfg_attr(
29    feature = "dto",
30    derive(IntoProto, FromProto),
31    proto(target = "proto::trigger_proto::TriggerStatus")
32)]
33#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
34#[cfg_attr(feature = "clap", clap(rename_all = "snake_case"))]
35#[serde(rename_all = "snake_case")]
36#[strum(serialize_all = "snake_case")]
37pub enum TriggerStatus {
38    Scheduled,
39    OnDemand,
40    Expired,
41    Cancelled,
42    Paused,
43}
44
45#[skip_serializing_none]
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47#[cfg_attr(feature = "validation", derive(Validate))]
48#[cfg_attr(
49    feature = "dto",
50    derive(IntoProto, FromProto),
51    proto(target = "proto::trigger_proto::Trigger")
52)]
53#[cfg_attr(feature = "server", serde(deny_unknown_fields))]
54pub struct Trigger {
55    // Ids are meant to be internal only, so they are neither accepted as input
56    // or outputed in the API. This is here just for IntoProto to work
57    #[cfg_attr(feature = "server", serde(skip))]
58    #[cfg(feature = "dto")]
59    pub id: Option<lib::types::TriggerId>,
60    #[cfg_attr(
61        feature = "validation",
62        validate(length(
63            min = 2,
64            max = 64,
65            message = "name must be between 2 and 64 characters if set"
66        ))
67    )]
68    #[cfg_attr(feature = "dto", proto(required))]
69    pub name: Option<String>,
70    pub description: Option<String>,
71    pub created_at: Option<DateTime<Utc>>,
72    pub updated_at: Option<DateTime<Utc>>,
73    #[cfg_attr(feature = "validation", validate)]
74    pub action: Option<Action>,
75    #[cfg_attr(feature = "validation", validate)]
76    pub schedule: Option<Schedule>,
77    pub status: Option<TriggerStatus>,
78    pub last_ran_at: Option<DateTime<Utc>>,
79    #[cfg_attr(feature = "validation", validate)]
80    pub payload: Option<Payload>,
81    // Estimate of timepoints of the next runs (up to 5 runs).
82    #[serde(default)]
83    pub estimated_future_runs: Vec<DateTime<Utc>>,
84}
85
86impl Trigger {
87    /// Returns the webhook if the action is a webhook
88    pub fn webhook(&self) -> Option<&Webhook> {
89        match self.action.as_ref() {
90            | Some(Action::Webhook(webhook)) => Some(webhook),
91            | _ => None,
92        }
93    }
94
95    /// Returns the recurring schedule if the schedule is of type `recurring`
96    pub fn recurring(&self) -> Option<&Recurring> {
97        match self.schedule.as_ref() {
98            | Some(Schedule::Recurring(r)) => Some(r),
99            | _ => None,
100        }
101    }
102
103    /// Returns the run_at schedule if the schedule is of type `timepoints`
104    pub fn run_at(&self) -> Option<&RunAt> {
105        match self.schedule.as_ref() {
106            | Some(Schedule::RunAt(r)) => Some(r),
107            | _ => None,
108        }
109    }
110}
111
112#[cfg(all(test, feature = "validation"))]
113mod tests {
114    use anyhow::Result;
115    use serde_json::json;
116
117    use super::*;
118
119    // test that TriggerStatus to_string output is snake_case
120    #[test]
121    fn trigger_status_to_string() {
122        assert_eq!("scheduled", TriggerStatus::Scheduled.to_string());
123        assert_eq!("on_demand", TriggerStatus::OnDemand.to_string());
124        assert_eq!("expired", TriggerStatus::Expired.to_string());
125        assert_eq!("cancelled", TriggerStatus::Cancelled.to_string());
126        assert_eq!("paused", TriggerStatus::Paused.to_string());
127    }
128
129    #[test]
130    fn validate_install_trigger_01() -> Result<()> {
131        std::env::set_var("CRONBACK__SKIP_PUBLIC_IP_VALIDATION", "true");
132
133        let request = json!(
134          {
135            "schedule": {
136              "type": "recurring",
137              "cron": "*/3 * * * * *",
138              "limit": 5
139            },
140            "action": {
141              "url": "http://localhost:3000/action",
142              "timeout_s": 10,
143              "retry": {
144                "delay_s": 100
145              }
146
147            }
148          }
149        );
150
151        let parsed: Trigger = serde_json::from_value(request)?;
152        parsed.validate()?;
153        std::env::remove_var("CRONBACK__SKIP_PUBLIC_IP_VALIDATION");
154        Ok(())
155    }
156}