#![warn(missing_docs)]
use std::fmt::{Display, Formatter};
use log::debug;
use reqwest::{Client, Response, StatusCode};
use serde::Deserialize;
const API_URL: &str = "https://api.isevenapi.xyz/api/iseven/";
#[cfg(feature = "blocking")]
pub fn is_even<T: Display>(number: T) -> bool {
IsEvenApiBlockingClient::new().get(number).unwrap().iseven()
}
#[cfg(feature = "blocking")]
pub fn is_odd<T: Display>(number: T) -> bool {
!is_even(number)
}
#[derive(Debug, Clone)]
pub struct IsEvenApiClient {
client: Client,
}
impl IsEvenApiClient {
pub fn new() -> Self {
Self::with_client(Client::new())
}
pub fn with_client(client: Client) -> Self {
debug!("Creating async HTTP client");
Self { client }
}
pub async fn get<T: Display>(&self, number: T) -> Result<IsEvenApiResponse, IsEvenApiError> {
let response = self.fetch_response(number).await?;
let status = response.status();
parse_response(response.json().await?, status)
}
pub async fn get_json<T: Display>(&self, number: T) -> Result<String, IsEvenApiError> {
let response = self.fetch_response(number).await?;
Ok(response.text().await.expect("Unable to decode response body"))
}
async fn fetch_response<T: Display>(&self, number: T) -> reqwest::Result<Response> {
let request_url = format!("{api_url}{num}", api_url = API_URL, num = number);
debug!("Fetching API response from {}", request_url);
self.client.get(request_url).send().await
}
}
impl Default for IsEvenApiClient {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "blocking")]
#[derive(Debug, Clone)]
pub struct IsEvenApiBlockingClient {
client: reqwest::blocking::Client,
}
#[cfg(feature = "blocking")]
impl IsEvenApiBlockingClient {
pub fn new() -> Self {
Self::with_client(reqwest::blocking::Client::new())
}
pub fn with_client(client: reqwest::blocking::Client) -> Self {
debug!("Creating blocking HTTP client");
Self { client }
}
pub fn get<T: Display>(&self, number: T) -> Result<IsEvenApiResponse, IsEvenApiError> {
let response = self.fetch_response(number)?;
let status = response.status();
parse_response(response.json()?, status)
}
pub fn get_json<T: Display>(&self, number: T) -> Result<String, IsEvenApiError> {
let response = self.fetch_response(number)?;
Ok(response.text().expect("Unable to decode response body"))
}
fn fetch_response<T: Display>(&self, number: T) -> reqwest::Result<reqwest::blocking::Response> {
let request_url = format!("{api_url}{num}", api_url = API_URL, num = number);
debug!("Fetching API response from {}", request_url);
self.client.get(request_url).send()
}
}
#[cfg(feature = "blocking")]
impl Default for IsEvenApiBlockingClient {
fn default() -> Self {
Self::new()
}
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct IsEvenApiResponse {
ad: String,
iseven: bool,
}
impl IsEvenApiResponse {
pub fn iseven(&self) -> bool {
self.iseven
}
pub fn ad(&self) -> &str {
&self.ad
}
pub fn isodd(&self) -> bool {
!self.iseven()
}
}
impl Display for IsEvenApiResponse {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", if self.iseven { "even" } else { "odd" })
}
}
#[derive(thiserror::Error, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[error("{}", self.error)]
pub struct IsEvenApiErrorResponse {
error: String,
}
impl IsEvenApiErrorResponse {
pub fn error(&self) -> &str {
&self.error
}
}
#[derive(thiserror::Error, Debug)]
pub enum IsEvenApiError {
#[error(transparent)]
NumberOutOfRange(IsEvenApiErrorResponse),
#[error(transparent)]
InvalidNumber(IsEvenApiErrorResponse),
#[error("Server returned status code {1}: {0}")]
UnknownErrorResponse(IsEvenApiErrorResponse, StatusCode),
#[error("network error: {0}")]
NetworkError(#[from] reqwest::Error),
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum IsEvenResponseType {
Ok(IsEvenApiResponse),
Err(IsEvenApiErrorResponse),
}
fn parse_response(
json: IsEvenResponseType,
status: StatusCode,
) -> Result<IsEvenApiResponse, IsEvenApiError> {
match json {
IsEvenResponseType::Ok(r) => Ok(r),
IsEvenResponseType::Err(e) => match status.as_u16() {
400 => Err(IsEvenApiError::InvalidNumber(e)),
401 => Err(IsEvenApiError::NumberOutOfRange(e)),
_ => Err(IsEvenApiError::UnknownErrorResponse(e, status)),
},
}
}
#[cfg(test)]
mod tests {
use crate::*;
const ODD_INTS: [i32; 5] = [1, 3, 5, 9, 5283];
const EVEN_INTS: [i32; 5] = [0, 2, 8, 10, 88888];
const OUT_OF_RANGE_INTS: [i32; 3] = [1000000, i32::MAX, -1];
const INVALID_INPUT: [&str; 4] = ["abc", "1.0.0", "hello world.as_u16()", "3.14"];
#[tokio::test]
async fn test_valid_int() {
let client = IsEvenApiClient::new();
for (&a, b) in ODD_INTS.iter().zip(EVEN_INTS) {
assert!(client.get(a).await.unwrap().isodd());
assert!(client.get(b).await.unwrap().iseven());
}
}
#[tokio::test]
async fn test_out_of_range() {
let client = IsEvenApiClient::new();
for &a in OUT_OF_RANGE_INTS.iter() {
assert!(client.get(a).await.is_err());
}
}
#[tokio::test]
async fn test_invalid_input() {
let client = IsEvenApiClient::new();
for &a in INVALID_INPUT.iter() {
assert!(client.get(a).await.is_err());
}
}
#[test]
#[cfg(feature = "blocking")]
fn test_valid_int_blocking() {
let client = IsEvenApiBlockingClient::new();
for (&a, b) in ODD_INTS.iter().zip(EVEN_INTS) {
assert!(client.get(a).unwrap().isodd());
assert!(client.get(b).unwrap().iseven());
}
}
#[test]
#[cfg(feature = "blocking")]
fn test_out_of_range_blocking() {
let client = IsEvenApiBlockingClient::new();
for &a in OUT_OF_RANGE_INTS.iter() {
assert!(client.get(a).is_err());
}
}
#[test]
#[cfg(feature = "blocking")]
fn test_invalid_input_blocking() {
let client = IsEvenApiBlockingClient::new();
for &a in INVALID_INPUT.iter() {
assert!(client.get(a).is_err());
}
}
}