zammad-api 0.4.0

Async, idiomatic Rust client for the Zammad Service Platform
Documentation
use async_stream::try_stream;
use futures_core::stream::BoxStream;
use reqwest::Method;
use serde::Serialize;
use serde_json::{Map, Value};

use crate::client::Client;
use crate::error::{Error, Result};
use crate::models::{NewOrganization, Organization};

const PATH: &str = "api/v1/organizations";

/// Organization endpoints.
#[derive(Debug)]
pub struct OrganizationsApi<'a> {
    client: &'a Client,
}

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

    /// `GET /api/v1/organizations/:id`.
    pub async fn get(&self, id: u64) -> Result<Organization> {
        let req = self.client.request(Method::GET, &format!("{PATH}/{id}"));
        self.client.send_json(req).await
    }

    /// `GET /api/v1/organizations` - paginated list.
    pub fn list(&self) -> ListOrganizations<'a> {
        ListOrganizations {
            client: self.client,
            per_page: 100,
        }
    }

    /// `GET /api/v1/organizations/search?query=…`.
    pub fn search(&self, query: impl Into<String>) -> SearchOrganizations<'a> {
        SearchOrganizations {
            client: self.client,
            query: query.into(),
            per_page: 100,
        }
    }

    /// `POST /api/v1/organizations` - create from a [`NewOrganization`].
    pub async fn create(&self, org: &NewOrganization) -> Result<Organization> {
        let req = self.client.request(Method::POST, PATH).json(org);
        self.client.send_json(req).await
    }

    /// `PUT /api/v1/organizations/:id` - accepts any [`Serialize`] patch.
    pub async fn update<P: Serialize + ?Sized>(&self, id: u64, patch: &P) -> Result<Organization> {
        let req = self
            .client
            .request(Method::PUT, &format!("{PATH}/{id}"))
            .json(patch);
        self.client.send_json(req).await
    }

    /// Set a single field and verify it was persisted server-side.
    /// See [`crate::api::TicketsApi::set_field`] for the rationale.
    pub async fn set_field(
        &self,
        id: u64,
        field: &str,
        value: impl Into<Value>,
    ) -> Result<Organization> {
        let value: Value = value.into();
        let mut body = Map::new();
        body.insert(field.to_string(), value.clone());
        let updated = self.update(id, &Value::Object(body)).await?;
        let actual = updated.extra.get(field).cloned();
        if actual.as_ref() != Some(&value) {
            return Err(Error::WriteVerificationFailed {
                field: field.into(),
                actual,
            });
        }
        Ok(updated)
    }

    /// `DELETE /api/v1/organizations/:id`.
    pub async fn delete(&self, id: u64) -> Result<()> {
        let req = self.client.request(Method::DELETE, &format!("{PATH}/{id}"));
        self.client.send_unit(req).await
    }
}

/// Builder + iterator for `GET /api/v1/organizations`.
#[derive(Debug)]
pub struct ListOrganizations<'a> {
    client: &'a Client,
    per_page: u32,
}

impl<'a> ListOrganizations<'a> {
    /// Page size (Zammad caps at 200).
    pub fn per_page(mut self, n: u32) -> Self {
        self.per_page = n;
        self
    }

    /// Fetch a single page.
    pub async fn page(&self, page: u32) -> Result<Vec<Organization>> {
        let req = self.client.request(Method::GET, PATH).query(&[
            ("page", page.to_string()),
            ("per_page", self.per_page.to_string()),
        ]);
        self.client.send_json(req).await
    }

    /// Yield every organization, fetching pages lazily.
    pub fn stream(self) -> BoxStream<'a, Result<Organization>> {
        let client = self.client;
        let per_page = self.per_page;
        Box::pin(try_stream! {
            let mut page = 1u32;
            loop {
                let req = client.request(Method::GET, PATH).query(&[
                    ("page", page.to_string()),
                    ("per_page", per_page.to_string()),
                ]);
                let chunk: Vec<Organization> = client.send_json(req).await?;
                let len = chunk.len() as u32;
                for o in chunk { yield o; }
                if len < per_page { break; }
                page += 1;
            }
        })
    }
}

/// Builder + iterator for `GET /api/v1/organizations/search`.
#[derive(Debug)]
pub struct SearchOrganizations<'a> {
    client: &'a Client,
    query: String,
    per_page: u32,
}

impl<'a> SearchOrganizations<'a> {
    /// Page size (Zammad caps at 200).
    pub fn per_page(mut self, n: u32) -> Self {
        self.per_page = n;
        self
    }

    /// Fetch a single page of search results.
    pub async fn page(&self, page: u32) -> Result<Vec<Organization>> {
        let req = self
            .client
            .request(Method::GET, &format!("{PATH}/search"))
            .query(&[
                ("query", self.query.clone()),
                ("page", page.to_string()),
                ("per_page", self.per_page.to_string()),
            ]);
        self.client.send_json(req).await
    }

    /// Yield every search hit, fetching pages lazily.
    pub fn stream(self) -> BoxStream<'a, Result<Organization>> {
        let client = self.client;
        let per_page = self.per_page;
        let query = self.query;
        Box::pin(try_stream! {
            let mut page = 1u32;
            loop {
                let req = client
                    .request(Method::GET, &format!("{PATH}/search"))
                    .query(&[
                        ("query", query.clone()),
                        ("page", page.to_string()),
                        ("per_page", per_page.to_string()),
                    ]);
                let chunk: Vec<Organization> = client.send_json(req).await?;
                let len = chunk.len() as u32;
                for o in chunk { yield o; }
                if len < per_page { break; }
                page += 1;
            }
        })
    }
}