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 struct WebHookPayloadArtifact {
84    /// The id of the webhook payload artifact
85    ///
86    /// This is the id of the webhook payload artifact. It is the id that is
87    /// used to identify the webhook payload artifact.
88    ///
89    pub id: Option<Uuid>,
90
91    /// The propagated payload
92    ///
93    /// This is the payload that is sent to the webhook. It should be a
94    /// serializable object. The key is flattened to the root of the object,
95    /// then the value is serialized as the value of the key.
96    ///
97    pub payload: String,
98
99    /// The trigger of the webhook
100    ///
101    /// This is the trigger of the webhook. It is the trigger that is used to
102    /// determine if the webhook should be executed.
103    ///
104    pub trigger: WebHookTrigger,
105
106    /// Propagation responses from the webhooks
107    ///
108    /// This is the response from the webhooks. It contains the url, status
109    /// code, and the body of the response. If the body is not present, it
110    /// should be `None` and should be skipped on serialization.
111    ///
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub propagations: Option<Vec<HookResponse>>,
114
115    /// Encrypted payload
116    ///
117    /// If the payload is encrypted, this should be set to true. Otherwise,
118    /// it should be set to false or None.
119    ///
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub encrypted: Option<bool>,
122
123    /// The number of attempts to dispatch the webhook
124    ///
125    /// This is the number of attempts to dispatch the webhook. It is the number
126    /// of attempts that have been made to dispatch the webhook.
127    ///
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub attempts: Option<u8>,
130
131    /// The attempted at timestamp
132    ///
133    /// This is the timestamp when the webhook payload artifact was attempted.
134    ///
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub attempted: Option<DateTime<Local>>,
137
138    /// The created at timestamp
139    ///
140    /// This is the timestamp when the webhook payload artifact was created.
141    ///
142    pub created: Option<DateTime<Local>>,
143
144    /// The status of the webhook execution
145    ///
146    /// This is the status of the webhook execution. It is the status that is
147    /// used to determine if the webhook should be executed.
148    ///
149    pub status: Option<WebHookExecutionStatus>,
150}
151
152impl WebHookPayloadArtifact {
153    pub fn new(
154        id: Option<Uuid>,
155        payload: String,
156        trigger: WebHookTrigger,
157    ) -> Self {
158        Self {
159            id,
160            payload,
161            trigger,
162            propagations: None,
163            encrypted: None,
164            attempts: None,
165            attempted: None,
166            created: None,
167            status: Some(WebHookExecutionStatus::Pending),
168        }
169    }
170
171    /// Encode payload as base64
172    ///
173    /// Stringify with serde and encode the payload as base64.
174    ///
175    pub fn encode_payload(&mut self) -> Result<Self, MappedErrors> {
176        let serialized_payload =
177            serde_json::to_string(&self.payload).map_err(|e| {
178                dto_err(format!("Failed to serialize payload: {}", e))
179            })?;
180
181        let encoded_payload =
182            general_purpose::STANDARD.encode(serialized_payload.as_bytes());
183
184        Ok(Self {
185            payload: encoded_payload,
186            ..self.clone()
187        })
188    }
189
190    /// Decode payload from base64
191    ///
192    /// Decode the payload from base64 and return the original payload.
193    ///
194    pub fn decode_payload(
195        &self,
196    ) -> Result<WebHookPayloadArtifact, MappedErrors> {
197        let decoded_payload =
198            match general_purpose::STANDARD.decode(&self.payload) {
199                Err(_) => return dto_err("Failed to decode base64").as_error(),
200                Ok(decoded) => String::from_utf8(decoded)
201                    .map_err(|_| dto_err("Failed to decode payload"))?,
202            };
203
204        let payload = serde_json::from_str(&decoded_payload).map_err(|e| {
205            dto_err(format!("Failed to deserialize payload: {}", e))
206        })?;
207
208        Ok(Self {
209            payload,
210            ..self.clone()
211        })
212    }
213}