iarapi-rs 0.1.1

A Rust library for interacting with the IAmResponding API.
Documentation
use async_trait::async_trait;
use serde::Deserialize;
use serde_json::json;
use std::error::Error;
use std::fmt;

/// A trait for abstracting the HTTP client implementation, allowing flexibility in runtime selection.
#[async_trait]
pub trait HttpClient {
    async fn get(&self, url: &str) -> Result<String, ApiError>;
    async fn post(&self, url: &str, json_body: &str) -> Result<String, ApiError>;
    async fn post_form(&self, url: &str, form_data: &[(&str, &str)]) -> Result<String, ApiError>;
}

/// Error type for API interactions, implemented with `std::error::Error`.
#[derive(Debug)]
pub enum ApiError {
    AuthenticationError,
    RequestError(String),
    ParsingError(String),
}

impl fmt::Display for ApiError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ApiError::AuthenticationError => write!(f, "Authentication error"),
            ApiError::RequestError(msg) => write!(f, "Request error: {}", msg),
            ApiError::ParsingError(msg) => write!(f, "Parsing error: {}", msg),
        }
    }
}

impl Error for ApiError {}

/// Main struct for interacting with the IAmResponding API.
pub struct IamRespondingAPI<C> {
    client: C,
    token_for_api: Option<String>, // Optional because it may not be populated until after login
    member_id: Option<i64>,         // Optional for the same reason
}

impl<C> IamRespondingAPI<C>
where
    C: HttpClient + Send + Sync,
{
    pub fn new(client: C) -> Self {
        Self {
            client,
            token_for_api: None,
            member_id: None,
        }
    }

    /// Logs in to the IAmResponding API and stores token and member_id for future requests.
    pub async fn login(
        &mut self,
        agency: &str,
        user: &str,
        pass: &str,
    ) -> Result<(), ApiError> {
        let login_params = json!({
            "memberLogin": true,
            "agencyName": agency,
            "memberfname": user,
            "memberpwd": pass,
            "rememberPwd": false,
            "urlTo": "",
            "overrideSession": true,
        });

        let serialized_params = serde_json::to_string(&login_params)
            .map_err(|e| ApiError::ParsingError(e.to_string()))?;

        let response = self
            .client
            .post(
                "https://iamresponding.com/v3/Pages/memberlogin.aspx/ValidateLoginInfo",
                &serialized_params,
            )
            .await?;

        if response.contains("The log-in information that you have entered is incorrect.") {
            return Err(ApiError::AuthenticationError);
        }

        // Example parsing of token and member ID from the response
        // Assuming `token_for_api` and `member_id` are found in the response body
        // Adjust based on actual response format
        let login_data: LoginResponse = serde_json::from_str(&response)
            .map_err(|e| ApiError::ParsingError(e.to_string()))?;
        
        self.token_for_api = Some(login_data.token_for_api);
        self.member_id = Some(login_data.member_id);

        Ok(())
    }

    /// Fetches the currently responding members.
    pub async fn get_now_responding(&self) -> Result<Vec<NowResponding>, ApiError> {
        let response = self
            .client
            .post_form("https://iamresponding.com/v3/AgencyServices.asmx/GetNowRespondingWithSort", &[])
            .await?;

        let data: NowRespondingResponse = serde_json::from_str(&response)
            .map_err(|e| ApiError::ParsingError(e.to_string()))?;

        Ok(data.now_responding)
    }

    /// Fetches scheduled members.
    pub async fn get_on_schedule(&self) -> Result<Vec<OnSchedule>, ApiError> {
        let response = self
            .client
            .post_form("https://iamresponding.com/v3/AgencyServices.asmx/GetOnScheduleWithSort", &[])
            .await?;

        let data: OnScheduleResponse = serde_json::from_str(&response)
            .map_err(|e| ApiError::ParsingError(e.to_string()))?;

        Ok(data.on_schedule)
    }

    /// Fetches dispatch messages.
    pub async fn list_dispatch_messages(&self) -> Result<Vec<DispatchMessage>, ApiError> {
        let response = self
            .client
            .post_form("https://iamresponding.com/v3/DispatchMessages.asmx/ListWithParser", &[])
            .await?;

        let data: DispatchMessageResponse = serde_json::from_str(&response)
            .map_err(|e| ApiError::ParsingError(e.to_string()))?;

        Ok(data.dispatch_messages)
    }

    /// Fetches specific incident information.
    pub async fn get_incident_info(&self, incident_id: i64) -> Result<IncidentInfoData, ApiError> {
        let params = json!({
            "messageID": incident_id,
            "token": self.token_for_api,
        });

        let serialized_params = serde_json::to_string(&params)
            .map_err(|e| ApiError::ParsingError(e.to_string()))?;

        let response = self
            .client
            .post("https://iamresponding.com/v3/agency/IncidentsDashboard.aspx/GetIncidentInfo", &serialized_params)
            .await?;

        let data: IncidentInfoResponse = serde_json::from_str(&response)
            .map_err(|e| ApiError::ParsingError(e.to_string()))?;

        data.data.into_iter().next().ok_or(ApiError::ParsingError("No data for incident".to_string()))
    }

    // Example of one of the methods using `token_for_api` and `member_id`
    pub async fn get_latest_incidents(&self) -> Result<Vec<IncidentInfoData>, ApiError> {
        let token = self.token_for_api.as_deref().ok_or(ApiError::AuthenticationError)?;
        let member_id = self.member_id.ok_or(ApiError::AuthenticationError)?;

        let params = json!({
            "memberID": member_id,
            "token": token,
        });

        let serialized_params = serde_json::to_string(&params)
            .map_err(|e| ApiError::ParsingError(e.to_string()))?;

        let response = self
            .client
            .post("https://iamresponding.com/v3/agency/IncidentsDashboard.aspx/GetLatestIncidents", &serialized_params)
            .await?;

        let data: IncidentInfoResponse = serde_json::from_str(&response)
            .map_err(|e| ApiError::ParsingError(e.to_string()))?;

        Ok(data.data)
    }

    /// Fetches reminders for scheduled events or meetings for a specific member.
    pub async fn get_reminders_by_member(&self) -> Result<Vec<EventReminder>, ApiError> {
        let token = self.token_for_api.as_deref().ok_or(ApiError::AuthenticationError)?;
        let member_id = self.member_id.ok_or(ApiError::AuthenticationError)?;

        let member_id_str = member_id.to_string();
        let days_str = "7".to_string();

        let form_data = [
            ("subsString", &member_id_str as &str),
            ("days", &days_str as &str),  // Convert to &str to match the expected type
        ];

        let response = self
            .client
            .post_form("https://iamresponding.com/v3/AgencyServices.asmx/GetRemindersByMember", &form_data)
            .await?;

        let data: RemindersResponse = serde_json::from_str(&response)
            .map_err(|e| ApiError::ParsingError(e.to_string()))?;

        Ok(data.reminders)
    }
}

// Login response struct to extract token and member_id from login response.
#[derive(Debug, Deserialize)]
struct LoginResponse {
    token_for_api: String,
    member_id: i64,
}

/// Data structure for deserializing response data.
#[derive(Debug, Deserialize)]
struct NowRespondingResponse {
    now_responding: Vec<NowResponding>,
}

#[derive(Debug, Deserialize)]
pub struct NowResponding {
    pub member_name: String,
}

#[derive(Debug, Deserialize)]
struct OnScheduleResponse {
    on_schedule: Vec<OnSchedule>,
}

#[derive(Debug, Deserialize)]
pub struct OnSchedule {
    pub member_name: String,
}

#[derive(Debug, Deserialize)]
struct DispatchMessageResponse {
    dispatch_messages: Vec<DispatchMessage>,
}

#[derive(Debug, Deserialize)]
pub struct DispatchMessage {
    pub message_body: String,
    pub address: Option<String>,
}

#[derive(Debug, Deserialize)]
struct IncidentInfoResponse {
    data: Vec<IncidentInfoData>,
}

#[derive(Debug, Deserialize)]
pub struct IncidentInfoData {
    pub id: i64,
    pub incident_type: String,
}

/// Data structure for deserializing response data from `get_reminders_by_member`.
#[derive(Debug, Deserialize)]
struct RemindersResponse {
    reminders: Vec<EventReminder>,
}

#[derive(Debug, Deserialize)]
pub struct EventReminder {
    pub event_name: String,
    pub date: String,
    pub time: String,
}