meilisearch_sdk/
webhooks.rs1use serde::Deserialize;
2use serde::{ser::SerializeMap, Serialize, Serializer};
3use std::collections::BTreeMap;
4use uuid::Uuid;
5
6#[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#[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27#[serde(rename_all = "camelCase")]
28pub struct WebhookList {
29 pub results: Vec<WebhookInfo>,
30}
31
32#[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 #[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 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 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#[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 pub fn with_url(&mut self, url: impl Into<String>) -> &mut Self {
92 self.url = Some(url.into());
93 self
94 }
95
96 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 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 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}