myc_core/domain/dtos/webhook/
responses.rs

1use super::WebHookTrigger;
2
3use base64::{engine::general_purpose, Engine};
4use chrono::{DateTime, Local};
5use mycelium_base::utils::errors::{dto_err, MappedErrors};
6use serde::{Deserialize, Serialize};
7use std::{fmt::Display, str::FromStr};
8use utoipa::ToSchema;
9use uuid::Uuid;
10
11#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
12#[serde(rename_all = "camelCase")]
13pub enum WebHookExecutionStatus {
14    /// The webhook execution is pending
15    ///
16    /// This is the status of the webhook execution when it is pending.
17    ///
18    Pending,
19
20    /// The webhook execution is successful
21    ///
22    /// This is the status of the webhook execution when it is successful.
23    ///
24    Success,
25
26    /// The webhook execution is failed
27    ///
28    /// This is the status of the webhook execution when it is failed.
29    ///
30    Failed,
31
32    /// The webhook execution is skipped
33    ///
34    /// This is the status of the webhook execution when it is skipped.
35    ///
36    Skipped,
37
38    /// The webhook execution is unknown
39    ///
40    /// This is the status of the webhook execution when it is unknown.
41    ///
42    Unknown,
43}
44
45impl Display for WebHookExecutionStatus {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            Self::Pending => write!(f, "pending"),
49            Self::Success => write!(f, "success"),
50            Self::Failed => write!(f, "failed"),
51            Self::Skipped => write!(f, "skipped"),
52            Self::Unknown => write!(f, "unknown"),
53        }
54    }
55}
56
57impl FromStr for WebHookExecutionStatus {
58    type Err = MappedErrors;
59
60    fn from_str(s: &str) -> Result<Self, Self::Err> {
61        match s {
62            "pending" => Ok(Self::Pending),
63            "success" => Ok(Self::Success),
64            "failed" => Ok(Self::Failed),
65            "skipped" => Ok(Self::Skipped),
66            "unknown" => Ok(Self::Unknown),
67            _ => dto_err("Invalid webhook execution status").as_error(),
68        }
69    }
70}
71
72#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
73#[serde(rename_all = "camelCase")]
74pub struct HookResponse {
75    pub url: String,
76    pub status: u16,
77    pub body: Option<String>,
78    pub datetime: DateTime<Local>,
79}
80
81#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
82#[serde(rename_all = "camelCase")]
83pub enum PayloadId {
84    Uuid(Uuid),
85    String(String),
86    Number(u64),
87}
88
89impl Display for PayloadId {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        match self {
92            Self::Uuid(uuid) => write!(f, "{}", uuid),
93            Self::String(string) => write!(f, "{}", string),
94            Self::Number(number) => write!(f, "{}", number),
95        }
96    }
97}
98
99impl FromStr for PayloadId {
100    type Err = MappedErrors;
101
102    fn from_str(s: &str) -> Result<Self, Self::Err> {
103        if let Ok(uuid) = s.parse::<Uuid>() {
104            Ok(Self::Uuid(uuid))
105        } else if let Ok(number) = s.parse::<u64>() {
106            Ok(Self::Number(number))
107        } else {
108            Ok(Self::String(s.to_string()))
109        }
110    }
111}
112
113#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
114#[serde(rename_all = "camelCase")]
115pub struct WebHookPayloadArtifact {
116    /// The id of the webhook payload artifact
117    ///
118    /// This is the id of the webhook payload artifact. It is the id that is
119    /// used to identify the webhook payload artifact.
120    ///
121    pub id: Option<Uuid>,
122
123    /// The propagated payload
124    ///
125    /// This is the payload that is sent to the webhook. It should be a
126    /// serializable object. The key is flattened to the root of the object,
127    /// then the value is serialized as the value of the key.
128    ///
129    pub payload: String,
130
131    /// The id of the payload
132    ///
133    /// This is the id of the payload. It is the id that is used to identify the
134    /// payload.
135    ///
136    pub payload_id: PayloadId,
137
138    /// The trigger of the webhook
139    ///
140    /// This is the trigger of the webhook. It is the trigger that is used to
141    /// determine if the webhook should be executed.
142    ///
143    pub trigger: WebHookTrigger,
144
145    /// Propagation responses from the webhooks
146    ///
147    /// This is the response from the webhooks. It contains the url, status
148    /// code, and the body of the response. If the body is not present, it
149    /// should be `None` and should be skipped on serialization.
150    ///
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub propagations: Option<Vec<HookResponse>>,
153
154    /// Encrypted payload
155    ///
156    /// If the payload is encrypted, this should be set to true. Otherwise,
157    /// it should be set to false or None.
158    ///
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub encrypted: Option<bool>,
161
162    /// The number of attempts to dispatch the webhook
163    ///
164    /// This is the number of attempts to dispatch the webhook. It is the number
165    /// of attempts that have been made to dispatch the webhook.
166    ///
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub attempts: Option<u8>,
169
170    /// The attempted at timestamp
171    ///
172    /// This is the timestamp when the webhook payload artifact was attempted.
173    ///
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub attempted: Option<DateTime<Local>>,
176
177    /// The created at timestamp
178    ///
179    /// This is the timestamp when the webhook payload artifact was created.
180    ///
181    pub created: Option<DateTime<Local>>,
182
183    /// The status of the webhook execution
184    ///
185    /// This is the status of the webhook execution. It is the status that is
186    /// used to determine if the webhook should be executed.
187    ///
188    pub status: Option<WebHookExecutionStatus>,
189}
190
191impl WebHookPayloadArtifact {
192    pub fn new(
193        id: Option<Uuid>,
194        payload: String,
195        payload_id: PayloadId,
196        trigger: WebHookTrigger,
197    ) -> Self {
198        Self {
199            id,
200            payload,
201            payload_id,
202            trigger,
203            propagations: None,
204            encrypted: None,
205            attempts: None,
206            attempted: None,
207            created: None,
208            status: Some(WebHookExecutionStatus::Pending),
209        }
210    }
211
212    /// Encode payload as base64
213    ///
214    /// Stringify with serde and encode the payload as base64.
215    ///
216    pub fn encode_payload(&mut self) -> Result<Self, MappedErrors> {
217        let serialized_payload =
218            serde_json::to_string(&self.payload).map_err(|e| {
219                dto_err(format!("Failed to serialize payload: {}", e))
220            })?;
221
222        let encoded_payload =
223            general_purpose::STANDARD.encode(serialized_payload.as_bytes());
224
225        Ok(Self {
226            payload: encoded_payload,
227            ..self.clone()
228        })
229    }
230
231    /// Decode payload from base64
232    ///
233    /// Decode the payload from base64 and return the original payload.
234    ///
235    pub fn decode_payload(
236        &self,
237    ) -> Result<WebHookPayloadArtifact, MappedErrors> {
238        let decoded_payload =
239            match general_purpose::STANDARD.decode(&self.payload) {
240                Err(_) => return dto_err("Failed to decode base64").as_error(),
241                Ok(decoded) => String::from_utf8(decoded)
242                    .map_err(|_| dto_err("Failed to decode payload"))?,
243            };
244
245        let payload = serde_json::from_str(&decoded_payload).map_err(|e| {
246            dto_err(format!("Failed to deserialize payload: {}", e))
247        })?;
248
249        Ok(Self {
250            payload,
251            ..self.clone()
252        })
253    }
254}