uni-sdk 0.3.0

The official Unimatrix SDK for Rust
Documentation
use std::collections::BTreeMap;

use reqwest::{header::HeaderMap, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::Value;

/// A successful Unimatrix API response.
#[derive(Clone, Debug)]
pub struct UniResponse<T> {
    /// HTTP status returned by the API.
    pub status: StatusCode,
    /// Unimatrix business response code. Successful responses use `"0"`.
    pub code: String,
    /// Human-readable response message.
    pub message: String,
    /// Typed response data, when supplied by the API.
    pub data: Option<T>,
    /// Request ID used for support and diagnostics.
    pub request_id: Option<String>,
    /// HTTP response headers.
    pub headers: HeaderMap,
    /// Complete parsed JSON response body.
    pub raw_body: Value,
}

impl<T> UniResponse<T> {
    /// Returns the response data or an error when the successful response did
    /// not contain a `data` field.
    pub fn into_data(self) -> crate::Result<T> {
        self.data.ok_or_else(|| crate::UniError::InvalidResponse {
            status: self.status,
            request_id: self.request_id,
            message: "response did not contain data".to_owned(),
            body: self.raw_body.to_string(),
        })
    }
}

/// One phone number or a list of phone numbers.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(untagged)]
pub enum Recipients {
    /// A single E.164 phone number.
    One(String),
    /// Multiple E.164 phone numbers.
    Many(Vec<String>),
}

impl From<String> for Recipients {
    fn from(value: String) -> Self {
        Self::One(value)
    }
}

impl From<&str> for Recipients {
    fn from(value: &str) -> Self {
        Self::One(value.to_owned())
    }
}

impl From<Vec<String>> for Recipients {
    fn from(value: Vec<String>) -> Self {
        Self::Many(value)
    }
}

impl<const N: usize> From<[&str; N]> for Recipients {
    fn from(value: [&str; N]) -> Self {
        Self::Many(value.into_iter().map(str::to_owned).collect())
    }
}

/// Parameters accepted by `sms.message.send`.
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SendMessageRequest {
    /// Destination phone number or phone numbers in E.164 format.
    pub to: Recipients,
    /// Message signature placed before or after content, depending on locale.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub signature: Option<String>,
    /// Unimatrix message template ID.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub template_id: Option<String>,
    /// Values substituted into the selected template.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub template_data: Option<BTreeMap<String, Value>>,
    /// Message content without a signature.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content: Option<String>,
    /// Complete message text, including any required signature.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub text: Option<String>,
}

impl SendMessageRequest {
    /// Creates a request whose text already includes the message signature.
    pub fn text(to: impl Into<Recipients>, text: impl Into<String>) -> Self {
        Self {
            to: to.into(),
            signature: None,
            template_id: None,
            template_data: None,
            content: None,
            text: Some(text.into()),
        }
    }

    /// Creates a request using a message template.
    pub fn template(
        to: impl Into<Recipients>,
        signature: impl Into<String>,
        template_id: impl Into<String>,
    ) -> Self {
        Self {
            to: to.into(),
            signature: Some(signature.into()),
            template_id: Some(template_id.into()),
            template_data: None,
            content: None,
            text: None,
        }
    }

    /// Creates a request using raw content and a separate signature.
    pub fn content(
        to: impl Into<Recipients>,
        signature: impl Into<String>,
        content: impl Into<String>,
    ) -> Self {
        Self {
            to: to.into(),
            signature: Some(signature.into()),
            template_id: None,
            template_data: None,
            content: Some(content.into()),
            text: None,
        }
    }

    /// Adds data used by a message template.
    pub fn with_template_data(mut self, data: BTreeMap<String, Value>) -> Self {
        self.template_data = Some(data);
        self
    }
}

/// Per-recipient message details returned by the API.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UniMessage {
    /// Unimatrix message ID.
    pub id: String,
    /// Recipient phone number.
    pub to: String,
    /// Recipient country ISO code.
    pub iso: String,
    /// Recipient calling code.
    pub cc: String,
    /// Number of billable message segments.
    pub count: u32,
    /// Price charged for the message.
    pub price: String,
}

/// Data returned after sending one or more messages.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SendMessageData {
    /// Number of recipients accepted by the API.
    pub recipients: u32,
    /// Total number of billable message segments.
    pub message_count: u32,
    /// ISO 4217 currency code used for amounts.
    pub currency: String,
    /// Total amount charged for the request.
    pub total_amount: String,
    /// Per-recipient message details.
    pub messages: Vec<UniMessage>,
}

/// Channel used to deliver a one-time passcode.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum OtpChannel {
    /// Lets Unimatrix select the delivery channel.
    Auto,
    /// Delivers the code by SMS.
    Sms,
    /// Delivers the code using a phone call.
    Call,
    /// Delivers the code using a voice message.
    Voice,
    /// Delivers the code using WhatsApp.
    Whatsapp,
}

/// Parameters accepted by `otp.send`.
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SendOtpRequest {
    /// Destination phone number in E.164 format.
    pub to: String,
    /// Caller-provided code. When omitted, Unimatrix generates one.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub code: Option<String>,
    /// Code lifetime in seconds.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ttl: Option<u32>,
    /// Number of digits in an automatically generated code.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub digits: Option<u8>,
    /// Application-defined verification intent.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub intent: Option<String>,
    /// Preferred delivery channel.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub channel: Option<OtpChannel>,
    /// Message signature used by an SMS template.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub signature: Option<String>,
    /// Message template ID used to deliver the code.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub template_id: Option<String>,
}

impl SendOtpRequest {
    /// Creates a request that lets Unimatrix generate and deliver the code.
    pub fn new(to: impl Into<String>) -> Self {
        Self {
            to: to.into(),
            code: None,
            ttl: None,
            digits: None,
            intent: None,
            channel: None,
            signature: None,
            template_id: None,
        }
    }

    /// Uses a caller-provided verification code.
    pub fn with_code(mut self, code: impl Into<String>) -> Self {
        self.code = Some(code.into());
        self
    }

    /// Sets the code lifetime in seconds.
    pub fn with_ttl(mut self, ttl: u32) -> Self {
        self.ttl = Some(ttl);
        self
    }

    /// Sets the number of digits in an automatically generated code.
    pub fn with_digits(mut self, digits: u8) -> Self {
        self.digits = Some(digits);
        self
    }

    /// Associates an application-defined intent with the verification.
    pub fn with_intent(mut self, intent: impl Into<String>) -> Self {
        self.intent = Some(intent.into());
        self
    }

    /// Selects the preferred delivery channel.
    pub fn with_channel(mut self, channel: OtpChannel) -> Self {
        self.channel = Some(channel);
        self
    }

    /// Selects an SMS template and its signature.
    pub fn with_template(
        mut self,
        signature: impl Into<String>,
        template_id: impl Into<String>,
    ) -> Self {
        self.signature = Some(signature.into());
        self.template_id = Some(template_id.into());
        self
    }
}

/// Parameters accepted by `otp.verify`.
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct VerifyOtpRequest {
    /// Phone number used when sending the code.
    pub to: String,
    /// Code supplied by the user.
    pub code: String,
    /// Maximum code age in seconds.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ttl: Option<u32>,
    /// Application-defined intent that must match the send request.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub intent: Option<String>,
}

impl VerifyOtpRequest {
    /// Creates a verification request for a phone number and code.
    pub fn new(to: impl Into<String>, code: impl Into<String>) -> Self {
        Self {
            to: to.into(),
            code: code.into(),
            ttl: None,
            intent: None,
        }
    }

    /// Sets the maximum accepted code age in seconds.
    pub fn with_ttl(mut self, ttl: u32) -> Self {
        self.ttl = Some(ttl);
        self
    }

    /// Requires the verification intent to match this value.
    pub fn with_intent(mut self, intent: impl Into<String>) -> Self {
        self.intent = Some(intent.into());
        self
    }
}

/// Data returned after verifying an OTP.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct VerifyOtpData {
    /// Phone number checked by the API.
    pub to: String,
    /// Whether the supplied code is valid.
    pub valid: bool,
}