parcelwing 0.1.0

Official Rust SDK for the Parcel Wing API.
Documentation
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;

use crate::error::Error;
use crate::http::HttpClient;
use crate::resources::{
    AutomationsResource, ContactsResource, EmailsResource, SegmentsResource, TopicsResource,
};

const DEFAULT_BASE_URL: &str = "https://parcelwing.com";
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);

/// Top-level Parcel Wing API client.
#[derive(Clone, Debug)]
pub struct Client {
    http: Arc<HttpClient>,
}

/// Options for creating a [`Client`].
#[derive(Clone, Debug)]
pub struct ClientOptions {
    pub api_key: String,
    pub base_url: Option<String>,
    pub timeout: Option<Duration>,
    pub default_headers: HashMap<String, String>,
}

impl Client {
    /// Create a new client using the production Parcel Wing API.
    pub fn new(api_key: impl Into<String>) -> Result<Self, Error> {
        Self::builder(api_key).build()
    }

    /// Create a new client from explicit options.
    pub fn with_options(options: ClientOptions) -> Result<Self, Error> {
        let mut builder = Self::builder(options.api_key);

        if let Some(base_url) = options.base_url {
            builder = builder.base_url(base_url);
        }

        if let Some(timeout) = options.timeout {
            builder = builder.timeout(timeout);
        }

        for (key, value) in options.default_headers {
            builder = builder.default_header(key, value);
        }

        builder.build()
    }

    /// Start building a client.
    pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
        ClientBuilder::new(api_key)
    }

    pub fn emails(&self) -> EmailsResource {
        EmailsResource::new(self.http.clone())
    }

    pub fn contacts(&self) -> ContactsResource {
        ContactsResource::new(self.http.clone())
    }

    pub fn segments(&self) -> SegmentsResource {
        SegmentsResource::new(self.http.clone())
    }

    pub fn topics(&self) -> TopicsResource {
        TopicsResource::new(self.http.clone())
    }

    pub fn automations(&self) -> AutomationsResource {
        AutomationsResource::new(self.http.clone())
    }
}

/// Builder for [`Client`].
#[derive(Debug)]
pub struct ClientBuilder {
    api_key: String,
    base_url: String,
    timeout: Duration,
    default_headers: HashMap<String, String>,
    http_client: Option<reqwest::Client>,
}

impl ClientBuilder {
    pub fn new(api_key: impl Into<String>) -> Self {
        Self {
            api_key: api_key.into(),
            base_url: DEFAULT_BASE_URL.to_string(),
            timeout: DEFAULT_TIMEOUT,
            default_headers: HashMap::new(),
            http_client: None,
        }
    }

    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
        self.base_url = base_url.into();
        self
    }

    pub fn timeout(mut self, timeout: Duration) -> Self {
        self.timeout = timeout;
        self
    }

    pub fn default_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.default_headers.insert(key.into(), value.into());
        self
    }

    pub fn http_client(mut self, http_client: reqwest::Client) -> Self {
        self.http_client = Some(http_client);
        self
    }

    pub fn build(self) -> Result<Client, Error> {
        let api_key = self.api_key.trim().to_string();
        if api_key.is_empty() {
            return Err(Error::Configuration(
                "Parcel Wing client requires a non-empty API key.".to_string(),
            ));
        }

        let base_url = self.base_url.trim().trim_end_matches('/').to_string();
        if base_url.is_empty() {
            return Err(Error::Configuration(
                "Parcel Wing client requires a non-empty base URL.".to_string(),
            ));
        }

        let http_client = match self.http_client {
            Some(client) => client,
            None => reqwest::Client::builder()
                .timeout(self.timeout)
                .build()
                .map_err(Error::HttpClientBuild)?,
        };

        Ok(Client {
            http: Arc::new(HttpClient::new(
                api_key,
                base_url,
                http_client,
                self.default_headers,
            )),
        })
    }
}