use reqwest::header::{self, HeaderMap, HeaderValue};
use crate::{Error, ItemType};
use self::{filter::Filter, pagination::Pagination, sort::Sort};
pub mod attributes;
pub mod filter;
pub mod pagination;
pub mod sort;
pub trait GetUrl {
fn get_url(&self) -> String;
}
pub struct RequestBuilder {
request: Request,
}
impl RequestBuilder {
pub fn new(item_type: ItemType) -> Self {
Self {
request: Request::new(item_type),
}
}
pub fn id(mut self, id: String) -> Self {
self.request.id = Some(id);
self
}
pub fn secondary_item_type(mut self, secondary_item_type: ItemType) -> Self {
self.request.secondary_item_type = Some(secondary_item_type);
self
}
pub fn sort(mut self, sort: Sort) -> Self {
self.request.sort = Some(sort);
self
}
pub fn filter(mut self, filter: Filter) -> Self {
self.request.filter = Some(filter);
self
}
pub fn pagination(mut self, pagination: Pagination) -> Self {
self.request.pagination = Some(pagination);
self
}
pub fn build(self) -> Result<Request, Error> {
let item_type = self.request.get_item_type();
if let Some(sort) = &self.request.sort {
if sort.get_item_type() != item_type {
return Err(Error::InvalidSort);
}
}
if let Some(filter) = &self.request.filter {
if filter.get_item_type() != item_type {
return Err(Error::InvalidFilter);
}
}
if self.request.secondary_item_type.is_some() && self.request.id.is_none() {
return Err(Error::InvalidSecondaryItemType);
}
Ok(self.request)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Request {
item_type: ItemType,
id: Option<String>,
secondary_item_type: Option<ItemType>,
sort: Option<Sort>,
filter: Option<Filter>,
pagination: Option<Pagination>,
}
impl Request {
pub(crate) fn new(item_type: ItemType) -> Self {
Self {
item_type,
id: None,
secondary_item_type: None,
sort: None,
filter: None,
pagination: None,
}
}
pub(crate) fn get_item_type(&self) -> ItemType {
if let Some(secondary_item_type) = &self.secondary_item_type {
secondary_item_type.clone()
} else {
self.item_type.clone()
}
}
}
impl GetUrl for Request {
fn get_url(&self) -> String {
let mut url = self.item_type.get_url();
if let Some(id) = &self.id {
url.push_str(&format!("/{}", id));
}
if let Some(secondary_item_type) = &self.secondary_item_type {
url.push_str(&format!("/{}", secondary_item_type.get_url()));
}
let mut aditional_url = vec![];
if let Some(sort) = &self.sort {
aditional_url.push(sort.get_url());
}
if let Some(filter) = &self.filter {
aditional_url.push(filter.get_url());
}
if let Some(pagination) = &self.pagination {
aditional_url.push(pagination.get_url());
}
if !aditional_url.is_empty() {
url.push('?');
url.push_str(&aditional_url.join("&"));
}
url
}
}
pub(crate) struct Requester {
token: String,
}
impl Requester {
pub(crate) fn new(token: String) -> Self {
Self { token }
}
pub(crate) async fn get(&self, url: &str) -> Result<String, reqwest::Error> {
let mut headers = HeaderMap::new();
headers.insert(
header::ACCEPT,
HeaderValue::from_str("application/json")
.expect("Failed to convert header to header value"),
);
headers.insert(
header::AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", self.token))
.expect("Failed to convert header to header value"),
);
let client = reqwest::Client::new();
match client
.get(format!("https://the-one-api.dev/v2/{}", url))
.headers(headers)
.send()
.await
{
Ok(response) => {
let response = response.error_for_status()?;
response.text().await
}
Err(e) => Err(e),
}
}
pub(crate) async fn get_from_request(
&self,
request: Request,
) -> Result<String, reqwest::Error> {
let url = request.get_url();
self.get(&url).await
}
}
#[cfg(test)]
mod tests {
use crate::{
attribute::{Attribute, BookAttribute, QuoteAttribute},
filter::Operator,
request::sort::SortOrder,
};
use super::*;
#[test]
fn test_simple_request_url() {
let request = RequestBuilder::new(ItemType::Book).build().unwrap();
assert_eq!(request.get_url(), "book");
}
#[test]
fn test_request_with_id_url() {
let request = RequestBuilder::new(ItemType::Book)
.id("123".to_string())
.build()
.unwrap();
assert_eq!(request.get_url(), "book/123");
}
#[test]
fn test_request_with_secondary_item_type_url() {
let request = RequestBuilder::new(ItemType::Book)
.secondary_item_type(ItemType::Chapter)
.build();
assert!(request.is_err());
let request = RequestBuilder::new(ItemType::Character)
.id("123".to_string())
.secondary_item_type(ItemType::Quote)
.build()
.unwrap();
assert_eq!(request.get_url(), "character/123/quote");
}
#[test]
fn test_request_with_sort_url() {
let request = RequestBuilder::new(ItemType::Book)
.sort(Sort::new(
SortOrder::Ascending,
Attribute::Book(BookAttribute::Name),
))
.build()
.unwrap();
assert_eq!(request.get_url(), "book?sort=name:asc");
}
#[test]
fn test_request_with_filter_url() {
let request = RequestBuilder::new(ItemType::Book)
.filter(Filter::Match(
Attribute::Book(BookAttribute::Name),
Operator::Eq,
vec!["The Fellowship of the Ring".to_string()],
))
.build()
.unwrap();
assert_eq!(request.get_url(), "book?name=The Fellowship of the Ring");
}
#[test]
fn test_request_with_pagination_url() {
let request = RequestBuilder::new(ItemType::Book)
.pagination(Pagination::new(10, 10, 2))
.build()
.unwrap();
assert_eq!(request.get_url(), "book?limit=10&offset=10&page=2");
}
#[test]
fn test_full_request_url() {
let request = RequestBuilder::new(ItemType::Character)
.id("123".to_string())
.secondary_item_type(ItemType::Quote)
.sort(Sort::new(
SortOrder::Ascending,
Attribute::Quote(QuoteAttribute::Dialog),
))
.filter(Filter::Match(
Attribute::Quote(QuoteAttribute::Dialog),
Operator::Eq,
vec!["Deagol!".to_string()],
))
.pagination(Pagination::new(10, 10, 2))
.build()
.unwrap();
assert_eq!(
request.get_url(),
"character/123/quote?sort=dialog:asc&dialog=Deagol!&limit=10&offset=10&page=2"
);
}
}