videosdk-server-sdk 0.1.0

Rust server SDK for the VideoSDK v2 REST APIs
Documentation
//! Webhooks delivering SIP call-event notifications.
//!
//! This manages webhook *configuration*. It is unrelated to verifying the
//! signature of an inbound webhook.

use std::sync::Arc;

use futures_util::Stream;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};

use crate::client::{CallOptions, Client};
use crate::common::MessageResponse;
use crate::error::{Error, Result};
use crate::pagination::{auto_page, paginate, ItemMapper, ListParams, Page, PageFetcher};
use crate::query::QueryBuilder;
use crate::resources::escape;
use crate::resources::sip::SipWebhookEvent;

const PATH: &str = "/v2/sip/webhooks";

/// A webhook delivering SIP call-event notifications.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SipWebhook {
    /// The webhook id.
    ///
    /// The list endpoint keys this `webhookId`; the SDK surfaces it here either way.
    #[serde(default, deserialize_with = "crate::common::null_to_default")]
    pub id: String,
    /// Where events are delivered.
    pub url: Option<String>,
    /// The subscribed events.
    #[serde(default, deserialize_with = "crate::common::null_to_default")]
    pub events: Vec<SipWebhookEvent>,
    /// When the webhook was created.
    pub created_at: Option<String>,
    /// When the webhook was last updated.
    pub updated_at: Option<String>,
    /// Any fields the server returned that this SDK does not model yet.
    #[serde(flatten)]
    pub extra: Map<String, Value>,
}

/// The parameters for creating or updating a SIP webhook.
#[derive(Debug, Clone, Default, Serialize)]
pub struct CreateSipWebhookParams {
    /// Where to deliver events.
    pub url: String,
    /// The events to subscribe to.
    pub events: Vec<SipWebhookEvent>,
}

/// The query parameters for [`SipWebhooksResource::list`].
#[derive(Debug, Clone, Default)]
pub struct SipWebhookListParams {
    /// The 1-based page number.
    pub page: Option<u32>,
    /// Items per page.
    pub per_page: Option<u32>,
    /// An opaque cursor from a previous page.
    pub cursor: Option<String>,
    /// Matches on the webhook's URL.
    pub search: Option<String>,
}

impl SipWebhookListParams {
    fn pagination(&self) -> ListParams {
        ListParams {
            page: self.page,
            per_page: self.per_page,
            cursor: self.cursor.clone(),
        }
    }
}

/// The list returns raw documents, which key the id as `webhookId`. Surface it
/// as `id`, so a listed webhook has the same shape as a fetched one.
fn webhook_item_mapper() -> ItemMapper<SipWebhook> {
    Arc::new(|value: Value| {
        let mut webhook: SipWebhook =
            serde_json::from_value(value).map_err(|e| Error::decode("SIP webhook list item", e))?;
        if webhook.id.is_empty() {
            if let Some(webhook_id) = webhook.extra.get("webhookId").and_then(Value::as_str) {
                webhook.id = webhook_id.to_string();
            }
        }
        Ok(webhook)
    })
}

/// SIP webhooks. Reached via [`SipResource::webhooks`](crate::SipResource::webhooks).
#[derive(Debug, Clone, Copy)]
pub struct SipWebhooksResource<'a> {
    client: &'a Client,
}

impl<'a> SipWebhooksResource<'a> {
    pub(crate) fn new(client: &'a Client) -> Self {
        Self { client }
    }

    /// Creates a webhook.
    pub async fn create(&self, params: CreateSipWebhookParams) -> Result<SipWebhook> {
        self.client
            .json(Method::POST, PATH, CallOptions::json(&params)?)
            .await
    }

    /// Lists webhooks, one page at a time.
    pub async fn list(&self, params: SipWebhookListParams) -> Result<Page<SipWebhook>> {
        paginate(
            self.fetcher(&params),
            &params.pagination(),
            "data",
            Some(webhook_item_mapper()),
        )
        .await
    }

    /// Lists webhooks, transparently fetching every page.
    pub fn list_stream(
        &self,
        params: SipWebhookListParams,
    ) -> impl Stream<Item = Result<SipWebhook>> + Send {
        auto_page(
            self.fetcher(&params),
            params.pagination(),
            "data",
            Some(webhook_item_mapper()),
        )
    }

    /// Fetches a webhook by id.
    pub async fn get(&self, webhook_id: &str) -> Result<SipWebhook> {
        let path = format!("{PATH}/{}", escape(webhook_id));
        self.client
            .json(Method::GET, &path, CallOptions::new())
            .await
    }

    /// Replaces a webhook's URL and events.
    pub async fn update(
        &self,
        webhook_id: &str,
        params: CreateSipWebhookParams,
    ) -> Result<SipWebhook> {
        let path = format!("{PATH}/{}", escape(webhook_id));
        self.client
            .json(Method::PATCH, &path, CallOptions::json(&params)?)
            .await
    }

    /// Deletes a webhook.
    pub async fn delete(&self, webhook_id: &str) -> Result<MessageResponse> {
        let path = format!("{PATH}/{}", escape(webhook_id));
        self.client
            .json(Method::DELETE, &path, CallOptions::new())
            .await
    }

    fn fetcher(&self, params: &SipWebhookListParams) -> PageFetcher {
        let client = self.client.clone();
        let search = params.search.clone();
        Arc::new(move |page, per_page| {
            let client = client.clone();
            let search = search.clone();
            Box::pin(async move {
                let query = QueryBuilder::new()
                    .opt("page", page)
                    .opt("perPage", per_page)
                    .opt_str("search", search.as_deref())
                    .into_pairs();
                client
                    .json::<Value>(Method::GET, PATH, CallOptions::new().query(query))
                    .await
            })
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn the_list_mapper_surfaces_webhook_id_as_id() {
        let mapper = webhook_item_mapper();
        let webhook = mapper(json!({"webhookId": "w-1", "url": "https://x"})).unwrap();
        assert_eq!(webhook.id, "w-1");

        let webhook = mapper(json!({"id": "real", "webhookId": "w-1"})).unwrap();
        assert_eq!(webhook.id, "real");
    }

    #[test]
    fn create_params_serialize_url_and_events() {
        let body = serde_json::to_value(CreateSipWebhookParams {
            url: "https://example.com/sip".into(),
            events: vec![SipWebhookEvent::CALL_STARTED, SipWebhookEvent::CALL_HANGUP],
        })
        .unwrap();
        assert_eq!(
            body,
            json!({"url": "https://example.com/sip", "events": ["call-started", "call-hangup"]})
        );
    }
}