meilisearch_sdk/
webhooks.rs

1use serde::Deserialize;
2use serde::{ser::SerializeMap, Serialize, Serializer};
3use std::collections::BTreeMap;
4use uuid::Uuid;
5
6/// Representation of a webhook configuration in Meilisearch.
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8#[serde(rename_all = "camelCase")]
9pub struct Webhook {
10    pub url: String,
11    #[serde(default)]
12    pub headers: BTreeMap<String, String>,
13}
14
15/// Metadata returned for each webhook by the Meilisearch API.
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(rename_all = "camelCase")]
18pub struct WebhookInfo {
19    pub uuid: Uuid,
20    pub is_editable: bool,
21    #[serde(flatten)]
22    pub webhook: Webhook,
23}
24
25/// Results wrapper returned by the `GET /webhooks` endpoint.
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27#[serde(rename_all = "camelCase")]
28pub struct WebhookList {
29    pub results: Vec<WebhookInfo>,
30}
31
32/// Payload used to create a new webhook.
33#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
34#[serde(rename_all = "camelCase")]
35pub struct WebhookCreate {
36    pub url: String,
37    #[serde(default)]
38    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
39    pub headers: BTreeMap<String, String>,
40}
41
42impl WebhookCreate {
43    /// Creates a new webhook payload with the given target URL.
44    #[must_use]
45    pub fn new(url: impl Into<String>) -> Self {
46        Self {
47            url: url.into(),
48            headers: BTreeMap::new(),
49        }
50    }
51
52    /// Adds or replaces an HTTP header that will be sent with the webhook request.
53    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
54        self.headers.insert(name.into(), value.into());
55        self
56    }
57
58    /// Adds or replaces an HTTP header in-place.
59    pub fn insert_header(
60        &mut self,
61        name: impl Into<String>,
62        value: impl Into<String>,
63    ) -> &mut Self {
64        self.headers.insert(name.into(), value.into());
65        self
66    }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Default)]
70enum HeadersUpdate {
71    #[default]
72    NotSet,
73    Reset,
74    Set(BTreeMap<String, Option<String>>),
75}
76
77/// Payload used to update or delete settings of an existing webhook.
78#[derive(Debug, Clone, Default, PartialEq, Eq)]
79pub struct WebhookUpdate {
80    url: Option<String>,
81    headers: HeadersUpdate,
82}
83
84impl WebhookUpdate {
85    #[must_use]
86    pub fn new() -> Self {
87        Self::default()
88    }
89
90    /// Updates the webhook target URL.
91    pub fn with_url(&mut self, url: impl Into<String>) -> &mut Self {
92        self.url = Some(url.into());
93        self
94    }
95
96    /// Adds or replaces an HTTP header to be sent with the webhook request.
97    pub fn set_header(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
98        match &mut self.headers {
99            HeadersUpdate::Set(map) => {
100                map.insert(name.into(), Some(value.into()));
101            }
102            _ => {
103                let mut map = BTreeMap::new();
104                map.insert(name.into(), Some(value.into()));
105                self.headers = HeadersUpdate::Set(map);
106            }
107        }
108        self
109    }
110
111    /// Removes a specific HTTP header from the webhook configuration.
112    pub fn remove_header(&mut self, name: impl Into<String>) -> &mut Self {
113        match &mut self.headers {
114            HeadersUpdate::Set(map) => {
115                map.insert(name.into(), None);
116            }
117            _ => {
118                let mut map = BTreeMap::new();
119                map.insert(name.into(), None);
120                self.headers = HeadersUpdate::Set(map);
121            }
122        }
123        self
124    }
125
126    /// Clears all HTTP headers associated with this webhook.
127    pub fn reset_headers(&mut self) -> &mut Self {
128        self.headers = HeadersUpdate::Reset;
129        self
130    }
131}
132
133impl Serialize for WebhookUpdate {
134    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
135    where
136        S: Serializer,
137    {
138        let mut field_count = 0;
139        if self.url.is_some() {
140            field_count += 1;
141        }
142        if !matches!(self.headers, HeadersUpdate::NotSet) {
143            field_count += 1;
144        }
145
146        let mut map = serializer.serialize_map(Some(field_count))?;
147        if let Some(url) = &self.url {
148            map.serialize_entry("url", url)?;
149        }
150        match &self.headers {
151            HeadersUpdate::NotSet => {}
152            HeadersUpdate::Reset => {
153                let none: Option<()> = None;
154                map.serialize_entry("headers", &none)?;
155            }
156            HeadersUpdate::Set(values) => {
157                map.serialize_entry("headers", values)?;
158            }
159        }
160        map.end()
161    }
162}
163
164#[cfg(test)]
165mod test {
166    use super::*;
167    use crate::client::Client;
168    use crate::errors::Error;
169    use meilisearch_test_macro::meilisearch_test;
170
171    #[test]
172    fn serialize_update_variants() {
173        let mut update = WebhookUpdate::new();
174        update.set_header("authorization", "token");
175        update.remove_header("referer");
176
177        let json = serde_json::to_value(&update).unwrap();
178        assert_eq!(
179            json,
180            serde_json::json!({
181                "headers": {
182                    "authorization": "token",
183                    "referer": null
184                }
185            })
186        );
187
188        let mut reset = WebhookUpdate::new();
189        reset.reset_headers();
190        let json = serde_json::to_value(&reset).unwrap();
191        assert_eq!(json, serde_json::json!({ "headers": null }));
192    }
193
194    #[meilisearch_test]
195    async fn webhook_crud(client: Client) -> Result<(), Error> {
196        let initial = client.get_webhooks().await?.results.len();
197
198        let unique_url = format!("https://example.com/webhooks/{}", Uuid::new_v4());
199
200        let mut create = WebhookCreate::new(unique_url.clone());
201        create
202            .insert_header("authorization", "SECURITY_KEY")
203            .insert_header("referer", "https://example.com");
204
205        let created = client.create_webhook(&create).await?;
206        assert_eq!(created.webhook.url, unique_url);
207        assert!(created.is_editable);
208        assert_eq!(created.webhook.headers.len(), 2);
209
210        let fetched = client.get_webhook(&created.uuid.to_string()).await?;
211        assert_eq!(fetched.uuid, created.uuid);
212
213        let mut update = WebhookUpdate::new();
214        update.remove_header("referer");
215        update.set_header("x-extra", "value");
216
217        let updated = client
218            .update_webhook(&created.uuid.to_string(), &update)
219            .await?;
220        assert!(!updated.webhook.headers.contains_key("referer"));
221        assert_eq!(
222            updated.webhook.headers.get("x-extra"),
223            Some(&"value".to_string())
224        );
225
226        let mut clear = WebhookUpdate::new();
227        clear.reset_headers();
228        let cleared = client
229            .update_webhook(&created.uuid.to_string(), &clear)
230            .await?;
231        assert!(cleared.webhook.headers.is_empty());
232
233        client.delete_webhook(&created.uuid.to_string()).await?;
234
235        let remaining = client.get_webhooks().await?;
236        assert!(
237            remaining.results.len() == initial
238                || !remaining.results.iter().any(|w| w.uuid == created.uuid)
239        );
240
241        Ok(())
242    }
243}