githubapi 0.1.0

Easy to use, strongly typed GitHub API consumer.
Documentation
use crate::types::{GitHubApiError, LimitRemainingReset};
use crate::Pagination;
use lazy_static::lazy_static;
use regex::Regex;
use reqwest::header::{HeaderMap, HeaderValue};
use serde::Deserialize;
use serde_json::error::Error as JsonError;

pub trait HeaderMapExtensions {
    fn get_as_u64(&self, key: &str) -> Option<u64>;
    fn get_rate_limits(&self) -> Option<LimitRemainingReset>;
    fn get_pagination(&self) -> Option<Vec<Pagination>>;
    fn get_next_page(&self) -> Option<u64>;
    fn is_ok(&self) -> bool;
}

impl HeaderMapExtensions for HeaderMap<HeaderValue> {
    fn get_as_u64(&self, key: &str) -> Option<u64> {
        match self.get(key) {
            Some(header_value) => match header_value.to_str() {
                Ok(string_value) => match string_value.parse() {
                    Ok(value) => Some(value),
                    _ => None,
                },
                _ => None,
            },
            _ => None,
        }
    }

    fn get_rate_limits(&self) -> Option<LimitRemainingReset> {
        let limit = self.get_as_u64("x-ratelimit-limit")?;
        let remaining = self.get_as_u64("x-ratelimit-remaining")?;
        let reset = self.get_as_u64("x-ratelimit-reset")?;

        Some(LimitRemainingReset {
            limit,
            remaining,
            reset,
        })
    }

    fn get_pagination(&self) -> Option<Vec<Pagination>> {
        lazy_static! {
            static ref RE: Regex = Regex::new(r#"<.*?[\?&]page=(\d+).*?>; rel="(.*?)""#).unwrap();
        }

        match self.get("Link") {
            None => None,
            Some(header) => match header.to_str() {
                Ok(data) => Some(
                    RE.captures_iter(data)
                        .map(|it| {
                            (
                                it.get(2).unwrap().as_str(),
                                it.get(1).unwrap().as_str().parse::<u64>().unwrap(),
                            )
                        })
                        .map(|(direction, number)| match direction {
                            "first" => Pagination::First(number),
                            "prev" => Pagination::Previous(number),
                            "next" => Pagination::Next(number),
                            "last" => Pagination::Last(number),
                            other => Pagination::Undefined(other.to_string(), number),
                        })
                        .collect::<Vec<_>>(),
                ),
                _ => None,
            },
        }
    }

    fn get_next_page(&self) -> Option<u64> {
        let result = self.get_pagination()?.into_iter().find(|it| match it {
            Pagination::Next(_) => true,
            _ => false,
        })?;

        if let Pagination::Next(value) = result {
            Some(value)
        } else {
            None
        }
    }

    fn is_ok(&self) -> bool {
        match self.get("status") {
            Some(value) => match value.to_str() {
                Ok(value) => match value.split_whitespace().next() {
                    Some(value) => value == "200",
                    _ => false,
                },
                _ => false,
            },
            _ => false,
        }
    }
}

pub fn parse_json<'a, T>(text: &'a str) -> Result<T, GitHubApiError>
where
    T: Deserialize<'a>,
{
    match serde_json::from_str(&text) {
        Ok(value) => Ok(value),
        Err(error) => Err(GitHubApiError::JsonError((error, text.to_string()))),
    }
}

pub trait ToJsonString {
    fn to_json_string(&self) -> Result<String, JsonError>;
}