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}