lotr_api/request/
mod.rs

1//! This module contains the structs that are used to make a request to the API.
2//! Here we define the [`Request`] struct and the [`RequestBuilder`] struct, which
3//! are the center of the custom request system.
4
5use reqwest::header::{self, HeaderMap, HeaderValue};
6
7use crate::{Error, ItemType};
8
9use self::{filter::Filter, pagination::Pagination, sort::Sort};
10
11pub mod attributes;
12pub mod filter;
13pub mod pagination;
14pub mod sort;
15
16/// This trait is implemented by all structs that can be used to make a request to the API.
17/// It is used to get the url for the request.
18pub trait GetUrl {
19    /// Returns the url that represents the struct's part of the request.
20    fn get_url(&self) -> String;
21}
22
23/// This struct is used to build a [`Request`].
24///
25/// # Example
26///
27/// ```
28/// use lotr_api::{Request, RequestBuilder, ItemType,
29///     sort::{Sort, SortOrder},
30///     request::GetUrl,
31///     attribute::{Attribute, BookAttribute}};
32///
33/// let request = RequestBuilder::new(ItemType::Book)
34///    .sort(Sort::new(
35///    SortOrder::Ascending,
36///    Attribute::Book(BookAttribute::Name),
37///    ))
38///    .build()
39///    .unwrap();
40///
41/// assert_eq!(request.get_url(), "book?sort=name:asc");
42/// ```
43pub struct RequestBuilder {
44    request: Request,
45}
46
47impl RequestBuilder {
48    pub fn new(item_type: ItemType) -> Self {
49        Self {
50            request: Request::new(item_type),
51        }
52    }
53
54    /// Sets the id of the request. This is used to get a specific item.
55    pub fn id(mut self, id: String) -> Self {
56        self.request.id = Some(id);
57        self
58    }
59
60    /// Sets the secondary item type of the request. If you wish
61    /// to get a secondary item type, you need to set the id of the request.\
62    /// If not the `build` function will return an error.
63    ///
64    /// # Example
65    /// ```
66    /// use lotr_api::{ItemType, Request, RequestBuilder,
67    ///     request::GetUrl};
68    ///
69    /// let request = RequestBuilder::new(ItemType::Character)
70    ///     .id("123".to_string())
71    ///     .secondary_item_type(ItemType::Quote)
72    ///     .build()
73    ///     .unwrap();
74    ///
75    /// assert_eq!(request.get_url(), "character/123/quote");
76    ///   ```
77    ///
78    pub fn secondary_item_type(mut self, secondary_item_type: ItemType) -> Self {
79        self.request.secondary_item_type = Some(secondary_item_type);
80        self
81    }
82
83    /// Sets the sort of the request. If you wish to sort the results
84    /// of the request, the `sort_by` attribute of the `Sort` struct
85    /// must be of the same type as the item type of the request ( or the
86    /// secondary item type if it is set).
87    ///
88    /// # Example
89    /// ```
90    /// use lotr_api::{ItemType, Request, RequestBuilder,
91    ///     attribute::{Attribute, BookAttribute},
92    ///     request::GetUrl,
93    ///     sort::{Sort, SortOrder}};
94    ///
95    /// let request = RequestBuilder::new(ItemType::Book)
96    ///     .sort(Sort::new(
97    ///         SortOrder::Ascending,
98    ///         Attribute::Book(BookAttribute::Name),
99    ///     ))
100    ///     .build()
101    ///     .unwrap();
102    ///
103    /// assert_eq!(request.get_url(), "book?sort=name:asc");
104    /// ```
105    /// Failing to match the item type of the request results in an error.
106    /// ```
107    /// use lotr_api::{ItemType, Request, RequestBuilder,
108    ///     attribute::{Attribute, BookAttribute},
109    ///     request::GetUrl,
110    ///     sort::{ Sort, SortOrder}};
111    ///
112    ///  let request = RequestBuilder::new(ItemType::Character)
113    ///     .id("123".to_string())
114    ///     .secondary_item_type(ItemType::Quote)
115    ///     .sort(Sort::new(
116    ///         SortOrder::Ascending,
117    ///         Attribute::Book(BookAttribute::Name),
118    ///     ))
119    ///     .build();
120    ///
121    /// assert!(request.is_err());
122    /// ```
123    ///
124    pub fn sort(mut self, sort: Sort) -> Self {
125        self.request.sort = Some(sort);
126        self
127    }
128
129    /// Sets the filter of the request. If you wish to filter the results
130    /// of the request, the `filter_by` attribute of the `Filter` struct
131    /// must be of the same type as the item type of the request ( or the
132    /// secondary item type if it is set).
133    ///
134    /// # Example
135    /// ```
136    /// use lotr_api::{ItemType, Request, RequestBuilder,
137    ///     attribute::{Attribute, BookAttribute},
138    ///     request::GetUrl,
139    ///     filter::{Filter, Operator}};
140    ///
141    /// let request = RequestBuilder::new(ItemType::Book)
142    ///     .filter(Filter::Match(
143    ///         Attribute::Book(BookAttribute::Name),
144    ///         Operator::Eq,
145    ///         vec!["The Fellowship of the Ring".to_string()],
146    ///     ))
147    ///     .build()
148    ///     .unwrap();
149    ///
150    /// assert_eq!(request.get_url(), "book?name=The Fellowship of the Ring");
151    /// ```
152    ///
153    /// Failing to match the item type of the request results in an error.
154    ///
155    /// ```
156    /// use lotr_api::{ItemType, Request, RequestBuilder,
157    ///     attribute::{Attribute, BookAttribute},
158    ///     request::GetUrl,
159    ///     filter::{Filter, Operator}};
160    ///
161    /// let request = RequestBuilder::new(ItemType::Character)
162    ///     .id("123".to_string())
163    ///     .secondary_item_type(ItemType::Quote)
164    ///     .filter(Filter::Match(
165    ///         Attribute::Book(BookAttribute::Name),
166    ///         Operator::Eq,
167    ///         vec!["The Fellowship of the Ring".to_string()],
168    ///     ))
169    ///     .build();
170    ///
171    /// assert!(request.is_err());
172    /// ```
173    pub fn filter(mut self, filter: Filter) -> Self {
174        self.request.filter = Some(filter);
175        self
176    }
177
178    /// Sets the pagination of the request.
179    pub fn pagination(mut self, pagination: Pagination) -> Self {
180        self.request.pagination = Some(pagination);
181        self
182    }
183
184    /// Builds the request. If the request is invalid, an error is returned.
185    ///
186    /// # Errors
187    ///
188    /// A request is invalid if:
189    /// - The secondary item type is set but the id is not.
190    /// - The sort is set but the item type of the sort does not match the item type of the request.
191    /// - The filter is set but the item type of the filter does not match the item type of the request.
192    pub fn build(self) -> Result<Request, Error> {
193        let item_type = self.request.get_item_type();
194        if let Some(sort) = &self.request.sort {
195            if sort.get_item_type() != item_type {
196                return Err(Error::InvalidSort);
197            }
198        }
199        if let Some(filter) = &self.request.filter {
200            if filter.get_item_type() != item_type {
201                return Err(Error::InvalidFilter);
202            }
203        }
204        // Every secondary item type needs an id.
205        if self.request.secondary_item_type.is_some() && self.request.id.is_none() {
206            return Err(Error::InvalidSecondaryItemType);
207        }
208
209        Ok(self.request)
210    }
211}
212
213/// This struct represents a request to the API.
214/// It should be created with the [`RequestBuilder`].
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct Request {
217    item_type: ItemType,
218    id: Option<String>,
219    secondary_item_type: Option<ItemType>,
220    sort: Option<Sort>,
221    filter: Option<Filter>,
222    pagination: Option<Pagination>,
223}
224
225impl Request {
226    pub(crate) fn new(item_type: ItemType) -> Self {
227        Self {
228            item_type,
229            id: None,
230            secondary_item_type: None,
231            sort: None,
232            filter: None,
233            pagination: None,
234        }
235    }
236
237    pub(crate) fn get_item_type(&self) -> ItemType {
238        if let Some(secondary_item_type) = &self.secondary_item_type {
239            secondary_item_type.clone()
240        } else {
241            self.item_type.clone()
242        }
243    }
244}
245
246impl GetUrl for Request {
247    fn get_url(&self) -> String {
248        let mut url = self.item_type.get_url();
249        if let Some(id) = &self.id {
250            url.push_str(&format!("/{}", id));
251        }
252        if let Some(secondary_item_type) = &self.secondary_item_type {
253            url.push_str(&format!("/{}", secondary_item_type.get_url()));
254        }
255
256        let mut aditional_url = vec![];
257        if let Some(sort) = &self.sort {
258            aditional_url.push(sort.get_url());
259        }
260        if let Some(filter) = &self.filter {
261            aditional_url.push(filter.get_url());
262        }
263        if let Some(pagination) = &self.pagination {
264            aditional_url.push(pagination.get_url());
265        }
266
267        if !aditional_url.is_empty() {
268            url.push('?');
269            url.push_str(&aditional_url.join("&"));
270        }
271
272        url
273    }
274}
275
276/// Wrapper for the [`reqwest::Client`] struct that contains the token
277/// and the actual url that is used to make the request.
278/// It is used to make requests to the API.
279pub(crate) struct Requester {
280    token: String,
281}
282
283impl Requester {
284    pub(crate) fn new(token: String) -> Self {
285        Self { token }
286    }
287
288    pub(crate) async fn get(&self, url: &str) -> Result<String, reqwest::Error> {
289        let mut headers = HeaderMap::new();
290        headers.insert(
291            header::ACCEPT,
292            HeaderValue::from_str("application/json")
293                .expect("Failed to convert header to header value"),
294        );
295        headers.insert(
296            header::AUTHORIZATION,
297            HeaderValue::from_str(&format!("Bearer {}", self.token))
298                .expect("Failed to convert header to header value"),
299        );
300
301        let client = reqwest::Client::new();
302        match client
303            .get(format!("https://the-one-api.dev/v2/{}", url))
304            .headers(headers)
305            .send()
306            .await
307        {
308            Ok(response) => {
309                let response = response.error_for_status()?;
310                response.text().await
311            }
312            Err(e) => Err(e),
313        }
314    }
315
316    pub(crate) async fn get_from_request(
317        &self,
318        request: Request,
319    ) -> Result<String, reqwest::Error> {
320        let url = request.get_url();
321        self.get(&url).await
322    }
323}
324
325#[cfg(test)]
326mod tests {
327
328    use crate::{
329        attribute::{Attribute, BookAttribute, QuoteAttribute},
330        filter::Operator,
331        request::sort::SortOrder,
332    };
333
334    use super::*;
335
336    #[test]
337    fn test_simple_request_url() {
338        let request = RequestBuilder::new(ItemType::Book).build().unwrap();
339        assert_eq!(request.get_url(), "book");
340    }
341
342    #[test]
343    fn test_request_with_id_url() {
344        let request = RequestBuilder::new(ItemType::Book)
345            .id("123".to_string())
346            .build()
347            .unwrap();
348        assert_eq!(request.get_url(), "book/123");
349    }
350
351    #[test]
352    fn test_request_with_secondary_item_type_url() {
353        let request = RequestBuilder::new(ItemType::Book)
354            .secondary_item_type(ItemType::Chapter)
355            .build();
356        assert!(request.is_err());
357
358        let request = RequestBuilder::new(ItemType::Character)
359            .id("123".to_string())
360            .secondary_item_type(ItemType::Quote)
361            .build()
362            .unwrap();
363
364        assert_eq!(request.get_url(), "character/123/quote");
365    }
366
367    #[test]
368    fn test_request_with_sort_url() {
369        let request = RequestBuilder::new(ItemType::Book)
370            .sort(Sort::new(
371                SortOrder::Ascending,
372                Attribute::Book(BookAttribute::Name),
373            ))
374            .build()
375            .unwrap();
376
377        assert_eq!(request.get_url(), "book?sort=name:asc");
378    }
379
380    #[test]
381    fn test_request_with_filter_url() {
382        let request = RequestBuilder::new(ItemType::Book)
383            .filter(Filter::Match(
384                Attribute::Book(BookAttribute::Name),
385                Operator::Eq,
386                vec!["The Fellowship of the Ring".to_string()],
387            ))
388            .build()
389            .unwrap();
390
391        assert_eq!(request.get_url(), "book?name=The Fellowship of the Ring");
392    }
393
394    #[test]
395    fn test_request_with_pagination_url() {
396        let request = RequestBuilder::new(ItemType::Book)
397            .pagination(Pagination::new(10, 10, 2))
398            .build()
399            .unwrap();
400
401        assert_eq!(request.get_url(), "book?limit=10&offset=10&page=2");
402    }
403
404    #[test]
405    fn test_full_request_url() {
406        let request = RequestBuilder::new(ItemType::Character)
407            .id("123".to_string())
408            .secondary_item_type(ItemType::Quote)
409            .sort(Sort::new(
410                SortOrder::Ascending,
411                Attribute::Quote(QuoteAttribute::Dialog),
412            ))
413            .filter(Filter::Match(
414                Attribute::Quote(QuoteAttribute::Dialog),
415                Operator::Eq,
416                vec!["Deagol!".to_string()],
417            ))
418            .pagination(Pagination::new(10, 10, 2))
419            .build()
420            .unwrap();
421
422        assert_eq!(
423            request.get_url(),
424            "character/123/quote?sort=dialog:asc&dialog=Deagol!&limit=10&offset=10&page=2"
425        );
426    }
427}