use serde::Deserialize;
use serde::{ser::SerializeMap, Serialize, Serializer};
use std::collections::BTreeMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Webhook {
pub url: String,
#[serde(default)]
pub headers: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WebhookInfo {
pub uuid: Uuid,
pub is_editable: bool,
#[serde(flatten)]
pub webhook: Webhook,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WebhookList {
pub results: Vec<WebhookInfo>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WebhookCreate {
pub url: String,
#[serde(default)]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub headers: BTreeMap<String, String>,
}
impl WebhookCreate {
#[must_use]
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
headers: BTreeMap::new(),
}
}
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(name.into(), value.into());
self
}
pub fn insert_header(
&mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> &mut Self {
self.headers.insert(name.into(), value.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
enum HeadersUpdate {
#[default]
NotSet,
Reset,
Set(BTreeMap<String, Option<String>>),
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct WebhookUpdate {
url: Option<String>,
headers: HeadersUpdate,
}
impl WebhookUpdate {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn with_url(&mut self, url: impl Into<String>) -> &mut Self {
self.url = Some(url.into());
self
}
pub fn set_header(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
match &mut self.headers {
HeadersUpdate::Set(map) => {
map.insert(name.into(), Some(value.into()));
}
_ => {
let mut map = BTreeMap::new();
map.insert(name.into(), Some(value.into()));
self.headers = HeadersUpdate::Set(map);
}
}
self
}
pub fn remove_header(&mut self, name: impl Into<String>) -> &mut Self {
match &mut self.headers {
HeadersUpdate::Set(map) => {
map.insert(name.into(), None);
}
_ => {
let mut map = BTreeMap::new();
map.insert(name.into(), None);
self.headers = HeadersUpdate::Set(map);
}
}
self
}
pub fn reset_headers(&mut self) -> &mut Self {
self.headers = HeadersUpdate::Reset;
self
}
}
impl Serialize for WebhookUpdate {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut field_count = 0;
if self.url.is_some() {
field_count += 1;
}
if !matches!(self.headers, HeadersUpdate::NotSet) {
field_count += 1;
}
let mut map = serializer.serialize_map(Some(field_count))?;
if let Some(url) = &self.url {
map.serialize_entry("url", url)?;
}
match &self.headers {
HeadersUpdate::NotSet => {}
HeadersUpdate::Reset => {
let none: Option<()> = None;
map.serialize_entry("headers", &none)?;
}
HeadersUpdate::Set(values) => {
map.serialize_entry("headers", values)?;
}
}
map.end()
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::client::Client;
use crate::errors::Error;
use meilisearch_test_macro::meilisearch_test;
#[test]
fn serialize_update_variants() {
let mut update = WebhookUpdate::new();
update.set_header("authorization", "token");
update.remove_header("referer");
let json = serde_json::to_value(&update).unwrap();
assert_eq!(
json,
serde_json::json!({
"headers": {
"authorization": "token",
"referer": null
}
})
);
let mut reset = WebhookUpdate::new();
reset.reset_headers();
let json = serde_json::to_value(&reset).unwrap();
assert_eq!(json, serde_json::json!({ "headers": null }));
}
#[meilisearch_test]
async fn webhook_crud(client: Client) -> Result<(), Error> {
let initial = client.get_webhooks().await?.results.len();
let unique_url = format!("https://example.com/webhooks/{}", Uuid::new_v4());
let mut create = WebhookCreate::new(unique_url.clone());
create
.insert_header("authorization", "SECURITY_KEY")
.insert_header("referer", "https://example.com");
let created = client.create_webhook(&create).await?;
assert_eq!(created.webhook.url, unique_url);
assert!(created.is_editable);
assert_eq!(created.webhook.headers.len(), 2);
let fetched = client.get_webhook(&created.uuid.to_string()).await?;
assert_eq!(fetched.uuid, created.uuid);
let mut update = WebhookUpdate::new();
update.remove_header("referer");
update.set_header("x-extra", "value");
let updated = client
.update_webhook(&created.uuid.to_string(), &update)
.await?;
assert!(!updated.webhook.headers.contains_key("referer"));
assert_eq!(
updated.webhook.headers.get("x-extra"),
Some(&"value".to_string())
);
let mut clear = WebhookUpdate::new();
clear.reset_headers();
let cleared = client
.update_webhook(&created.uuid.to_string(), &clear)
.await?;
assert!(cleared.webhook.headers.is_empty());
client.delete_webhook(&created.uuid.to_string()).await?;
let remaining = client.get_webhooks().await?;
assert!(
remaining.results.len() == initial
|| !remaining.results.iter().any(|w| w.uuid == created.uuid)
);
Ok(())
}
}