whos_your_daddy_common 0.1.3

Common source code for the Who's Your Daddy projects, like the Enumerator and Presenter.
Documentation
//! The `congress_gov` crate contains definitions for interacting with the
//! www.gpo.congress.gov API.

// Custom crates.
use crate::error::Error;

// Public crates.
use reqwest;
use serde::Deserialize;
use std::{collections::VecDeque, env};

/// Name of the environment variable containing the API key for communicating
/// with the Congress.gov API.
const ENV_API_KEY: &str = "CONGRESS_GOV_API_KEY";

/// This constant defines the root URL for the Congress.gov API.
const ROOT_URL: &str = "https://api.congress.gov/v3";

/// This constant defines the path for requesting Congressional members from.
const MEMBERS_PATH: &str = "/member";

/// This constant defines the URL parameter string for specifying the API key
/// in requests to Congress.gov.
const API_KEY_URL_PARAM: &str = "api_key=";

//==============================================================================
// Message structures
//==============================================================================

/// This structure represents a Congressional member's depiction data from
/// Congress.gov.
#[derive(Clone, Debug, Deserialize)]
pub struct Depiction {
    /// This field contains a URL to where the Congressional member's image
    /// was sourced from.
    #[serde(rename = "attribution")]
    pub attribution: Option<String>,

    /// This field specifies the URL where the Congressional member's image
    /// is hosted.
    #[serde(rename = "imageUrl")]
    pub image_url: Option<String>,
}

/// This struct represents pagination data in responses from Congress.gov.
#[derive(Clone, Debug, Deserialize)]
pub struct Pagination {
    /// This field indicates the number of available records to page through.
    #[serde(rename = "count")]
    pub count: i32,

    /// This field specifies the next page of results available from the API.
    #[serde(rename = "next")]
    pub next: Option<String>,
}

/// This struct represents a Congressional member's single term of service.
#[derive(Clone, Debug, Deserialize)]
pub struct TermItem {
    /// This field indicates where the Congressional member served.
    #[serde(rename = "chamber")]
    pub chamber: String,

    /// This field specifies when the Congressional member's term ended.
    #[serde(rename = "endYear")]
    pub end_year: Option<i32>,

    /// This field specifies when the Congressional member's term began.
    #[serde(rename = "startYear")]
    pub start_year: i32,
}

/// The `Terms` struct contains a list of `TermItem`s.
#[derive(Clone, Debug, Deserialize)]
pub struct Terms {
    /// The terms of service related to a single member.
    pub item: Vec<TermItem>,
}

/// The `Member` struct manages all the information about a single
/// Congressional member.
#[derive(Clone, Debug, Deserialize)]
pub struct Member {
    /// The `bioguide_id` field contains a unique identifier.
    #[serde(rename = "bioguideId")]
    pub bioguide_id: String,

    /// `Depiction` instance for this Congressional member.
    #[serde(rename = "depiction")]
    pub depiction: Option<Depiction>,

    /// The `district` field indicates the Representative's home district, if
    /// applicable.
    #[serde(rename = "district")]
    pub district: Option<i32>,

    /// This is the Congressional member's name.
    #[serde(rename = "name")]
    pub name: String,

    /// This field indicates the member's political party name.
    #[serde(rename = "partyName")]
    pub party_name: String,

    /// This field contains the name of the specific U.S. State where the
    /// Congressional member represents.
    #[serde(rename = "state")]
    pub state: String,

    /// This field contains the member's terms of service.
    #[serde(rename = "terms")]
    pub terms: Terms,

    /// This field indicates the most recent data when this member's record was
    /// updated.
    #[serde(rename = "updateDate")]
    pub update_date: String,

    /// The `url` field contains a URL pointing to the current Congressional
    /// member's record within the congress.gov API.
    #[serde(rename = "url")]
    pub url: String,
}

impl Member {
    /// The `split_name` method takes the `name` field in the referenced
    /// `Member` struct and splits it into its component strings.
    ///
    /// # Returns
    /// This function returns a `Result` variant:
    /// - `Ok()` containing a tuple of the name components (first, middle,
    ///   last).
    /// - `Err()` describing the error.
    pub fn split_name(&self) -> Result<(String, Option<String>, String), Error> {
        let first_name: &str;
        let middle_name: Option<String>;

        // Check for last name comma separateor.
        if !self.name.contains(",") {
            return Err(Error::NameSplitError(format!(
                "The name {0} is not in the expected format",
                self.name
            )));
        }

        let mut parts: VecDeque<&str> = self.name.split(',').collect();

        if parts.len() < 2 {
            return Err(Error::NameSplitError(format!(
                "The name {0} did not split into the expected number of segments",
                self.name
            )));
        }

        let last_name_option = parts.pop_front();
        let second_segment_option = parts.pop_front();

        let last_name: &str = match last_name_option {
            Some(last) => last,
            None => {
                return Err(Error::NameSplitError(String::from(
                    "Failed to obtain the last name.",
                )));
            }
        };

        //======================================================================
        // Parse the first and middle names.
        let second_segment = match second_segment_option {
            Some(second_segment) => second_segment.trim_start(),
            None => {
                return Err(Error::NameSplitError(format!(
                    "The name {0} did not contain a first or middle name",
                    self.name
                )));
            }
        };

        if second_segment.contains(' ') {
            let space_index = second_segment.find(' ').unwrap();

            let (first, middle) = second_segment.split_at(space_index);

            first_name = first;

            middle_name = if middle.is_empty() {
                None
            } else {
                Some(String::from(middle))
            };
        } else {
            first_name = second_segment;
            middle_name = None;
        }

        //======================================================================
        // Final validation.
        if first_name.is_empty() {
            Err(Error::NameSplitError(String::from(
                "Failed to parse the first name segment.",
            )))
        } else if last_name.is_empty() {
            Err(Error::NameSplitError(String::from(
                "Failed to parse the last name segment.",
            )))
        } else {
            Ok((
                String::from(first_name),
                middle_name,
                String::from(last_name),
            ))
        }
    } // end split_name
}

/// The `MembersMessage` represents the HTTP response we expect to receive from
/// congress.gov after a successful request.
#[derive(Clone, Debug, Deserialize)]
pub struct MembersMessage {
    /// The `members` field is a list of Congressional Member objects from
    /// the server.
    #[serde(rename = "members")]
    pub members: Vec<Member>,

    /// The `pagination` field contains a data to page through Congressional
    /// member data on the server.
    #[serde(rename = "pagination")]
    pub pagination: Pagination,
}

/// The `get_members` function sends a request to the congress.gov server for
/// Congressional members.
///
/// # Parameters
/// ## `offset`
/// The `offset` parameter specifies the number of entries within the server to
/// skip over.  An offset of 0 will begin with the first record.
///
/// ## `limit`
/// The `limit` parameter specifies the maximum number of member records to
/// respond with.
///
/// # Returns
/// This function returns a Result variant:
/// - `Ok()` a vector of `Member` structs for each Member received from the
///   server.
/// - `Err()` describing the error.
pub async fn get_members(offset: i32, limit: i32) -> Result<Vec<Member>, Error> {
    let api_key = match env::var(ENV_API_KEY) {
        Ok(key) => key,
        Err(e) => return Err(Error::CongressGovApiKeyError(e.to_string())),
    };

    let url = format!(
        "{ROOT_URL}{MEMBERS_PATH}?{API_KEY_URL_PARAM}{api_key}&offset={offset}&limit={limit}"
    );

    match reqwest::get(url).await {
        Ok(response) => {

            match response.text().await {
                Ok(body) => {

                    match serde_json::from_str::<MembersMessage>(&body) {
                        Ok(member_struct) => Ok(member_struct.members),
                        Err(e) => {
                            Err(Error::CongressGovResponseError(
                                format!(
                                    "{}\nFailed to deserialize the response body into a MembersMessage structure: {}",
                                    body,
                                    e
                                )
                            ))
                        },
                    }
                },
                Err(e) => Err(Error::CongressGovResponseError(e.to_string())),
            }
        },
        Err(e) => Err(Error::CongressGovHttpGetError(e.to_string())),
    }
} // get_members