bbb_api_wrapper/
http.rs

1// Copyright (c) 2021 Harry [Majored] [hello@majored.pw]
2// MIT License (https://github.com/Majored/rs-bbb-api-wrapper/blob/main/LICENSE)
3
4//! Holds key types and methods for handling raw HTTP requests to the API.
5
6// As we need to be able to resend the request if we hit a rate limit, we need to either:
7// - use a loop
8// - use as async recursive function
9//
10// The latter would require the addition of an indirection via a boxed future due to infinitely-sized types. This
11// approach lacks consistency with the rest of this wrapper and is harder to maintain. We've gone with the former
12// where the outer loop controls the request retry, and the inner loop controls the stalling retry.
13
14use crate::error::APIError;
15use crate::error::Result;
16use crate::throttler::{RateLimitStore, RequestType};
17use crate::APIWrapper;
18
19use reqwest::{Response, StatusCode};
20use serde::{de::DeserializeOwned, Deserialize, Serialize};
21use tokio::time::Duration;
22
23/// A structure representing a parsed response from the API.
24#[derive(Deserialize)]
25pub struct APIResponse<D> {
26    pub result: String,
27    pub data: Option<D>,
28    pub error: Option<APIError>,
29}
30
31impl<D> APIResponse<D> {
32    /// Returns whether or not the response was successful.
33    pub fn is_success(&self) -> bool {
34        self.result == "success"
35    }
36
37    /// Returns the containing data within the response.
38    ///
39    /// Will panic if the response was not successful.
40    pub fn data(self) -> D {
41        self.data.expect("no data present")
42    }
43
44    /// Returns the containing error within the response.
45    ///
46    /// Will panic if the response was successful.
47    pub fn error(self) -> APIError {
48        self.error.expect("no error present")
49    }
50
51    pub fn as_result(self) -> Result<D> {
52        if self.is_success() {
53            Ok(self.data())
54        } else {
55            Err(self.error())
56        }
57    }
58}
59
60pub async fn get<D>(wrapper: &APIWrapper, endpoint: &str) -> Result<APIResponse<D>>
61where
62    D: DeserializeOwned,
63{
64    loop {
65        loop {
66            match crate::throttler::stall_for(&wrapper.rate_limit_store, RequestType::READ) {
67                0 => break,
68                stall_for => tokio::time::sleep(Duration::from_millis(stall_for)).await,
69            };
70        }
71
72        let response = wrapper.http_client.get(endpoint).send().await?;
73
74        if !did_hit_limit(&wrapper.rate_limit_store, &response, RequestType::READ) {
75            return Ok(response.json().await?);
76        }
77    }
78}
79
80pub async fn post<D, B>(wrapper: &APIWrapper, endpoint: &str, body: &B) -> Result<APIResponse<D>>
81where
82    D: DeserializeOwned,
83    B: Serialize,
84{
85    loop {
86        loop {
87            match crate::throttler::stall_for(&wrapper.rate_limit_store, RequestType::WRITE) {
88                0 => break,
89                stall_for => tokio::time::sleep(Duration::from_millis(stall_for)).await,
90            };
91        }
92
93        let response = wrapper.http_client.post(endpoint).json(body).send().await?;
94
95        if !did_hit_limit(&wrapper.rate_limit_store, &response, RequestType::WRITE) {
96            return Ok(response.json().await?);
97        }
98    }
99}
100
101pub async fn patch<D, B>(wrapper: &APIWrapper, endpoint: &str, body: &B) -> Result<APIResponse<D>>
102where
103    D: DeserializeOwned,
104    B: Serialize,
105{
106    loop {
107        loop {
108            match crate::throttler::stall_for(&wrapper.rate_limit_store, RequestType::WRITE) {
109                0 => break,
110                stall_for => tokio::time::sleep(Duration::from_millis(stall_for)).await,
111            };
112        }
113
114        let response = wrapper.http_client.post(endpoint).json(body).send().await?;
115
116        if !did_hit_limit(&wrapper.rate_limit_store, &response, RequestType::WRITE) {
117            return Ok(response.json().await?);
118        }
119    }
120}
121
122pub async fn delete<D>(wrapper: &APIWrapper, endpoint: &str) -> Result<APIResponse<D>>
123where
124    D: DeserializeOwned,
125{
126    loop {
127        loop {
128            match crate::throttler::stall_for(&wrapper.rate_limit_store, RequestType::WRITE) {
129                0 => break,
130                stall_for => tokio::time::sleep(Duration::from_millis(stall_for)).await,
131            };
132        }
133
134        let response = wrapper.http_client.delete(endpoint).send().await?;
135
136        if !did_hit_limit(&wrapper.rate_limit_store, &response, RequestType::WRITE) {
137            return response.json().await?;
138        }
139    }
140}
141
142fn did_hit_limit(store: &RateLimitStore, response: &Response, request_type: RequestType) -> bool {
143    if response.status() != StatusCode::TOO_MANY_REQUESTS {
144        match &request_type {
145            RequestType::READ => store.reset_read(),
146            RequestType::WRITE => store.reset_write(),
147        };
148
149        return false;
150    }
151
152    let retry = response.headers().get("Retry-After").expect("no retry-after header present");
153    let retry: u64 = retry.to_str().expect("non-ascii characters present").parse().expect("not a valid u64 int");
154
155    match &request_type {
156        RequestType::READ => store.store_read(retry),
157        RequestType::WRITE => store.store_write(retry),
158    };
159
160    true
161}