use reqwest::Method;
use reqwest::header::HeaderMap;
use crate::error::{Error, Result};
#[derive(Debug)]
pub(crate) struct RawResponse {
#[allow(dead_code)]
pub status: u16,
pub headers: HeaderMap,
pub body: bytes::Bytes,
}
pub(crate) fn decode_json<T: serde::de::DeserializeOwned>(body: &[u8]) -> Result<T> {
serde_json::from_slice(body).map_err(|e| Error::Decode(e.to_string()))
}
pub(crate) fn room_path(room: &str, suffix: &str) -> String {
format!("/chat/v4/rooms/{}{}", urlencoding::encode(room), suffix)
}
pub(crate) fn message_path(room: &str, serial: &str, suffix: &str) -> String {
format!(
"/chat/v4/rooms/{}/messages/{}{}",
urlencoding::encode(room),
urlencoding::encode(serial),
suffix
)
}
impl crate::client::Inner {
fn retry_eligible(method: &Method, has_idempotency_key: bool) -> bool {
matches!(*method, Method::GET | Method::DELETE) || has_idempotency_key
}
pub(crate) async fn send(
&self,
method: Method,
path: &str,
query: &[(&str, String)],
body: Option<serde_json::Value>,
has_idem: bool,
) -> Result<RawResponse> {
let url = format!("{}{}", self.base, path);
self.send_url(method, url, query, body, has_idem).await
}
pub(crate) async fn send_url(
&self,
method: Method,
url: String,
query: &[(&str, String)],
body: Option<serde_json::Value>,
has_idem: bool,
) -> Result<RawResponse> {
let eligible = Self::retry_eligible(&method, has_idem);
let mut attempt = 0;
loop {
let mut req = self
.http
.request(method.clone(), &url)
.header("X-Ably-Version", "4")
.header(reqwest::header::AUTHORIZATION, self.auth_header.as_str());
if !query.is_empty() {
req = req.query(query);
}
if let Some(b) = &body {
req = req.json(b);
}
match req.send().await {
Ok(r) => {
let status = r.status().as_u16();
if (status == 429 || (500..=599).contains(&status))
&& eligible
&& attempt < self.max_retries
{
attempt += 1;
continue;
}
let headers = r.headers().clone();
let bytes = r.bytes().await?;
if (200..300).contains(&status) {
return Ok(RawResponse {
status,
headers,
body: bytes,
});
}
return Err(Error::from_api_body(status, &bytes));
}
Err(e) => {
if e.is_timeout() && eligible && attempt < self.max_retries {
attempt += 1;
continue;
}
return Err(e.into());
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::{Client, Inner};
use crate::config::Auth;
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[test]
fn path_helpers_url_encode_segments() {
assert_eq!(
room_path("my room", "/occupancy"),
"/chat/v4/rooms/my%20room/occupancy"
);
assert_eq!(
message_path("r", "01ts-001@abc:001", "/versions"),
"/chat/v4/rooms/r/messages/01ts-001%40abc%3A001/versions"
);
}
#[test]
fn retry_eligible_predicate() {
assert!(Inner::retry_eligible(&Method::GET, false));
assert!(Inner::retry_eligible(&Method::DELETE, false));
assert!(!Inner::retry_eligible(&Method::POST, false));
assert!(Inner::retry_eligible(&Method::POST, true));
assert!(!Inner::retry_eligible(&Method::PUT, false));
assert!(Inner::retry_eligible(&Method::PUT, true));
}
#[tokio::test]
async fn sends_version_and_auth_headers_and_maps_api_error() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/chat/v4/rooms/r/occupancy"))
.and(header("x-ably-version", "4"))
.and(header("authorization", "Basic YXBwLms6cw=="))
.respond_with(
ResponseTemplate::new(404)
.set_body_string(r#"{"error":{"code":40400,"message":"no","statusCode":404}}"#),
)
.expect(1)
.mount(&server)
.await;
let client = Client::builder(Auth::api_key("app.k:s"))
.host(server.uri())
.build();
let err = client
.inner
.send(Method::GET, "/chat/v4/rooms/r/occupancy", &[], None, false)
.await
.unwrap_err();
assert!(err.is_not_found());
assert_eq!(err.status(), Some(404));
}
#[tokio::test]
async fn success_returns_status_and_body_bytes() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/chat/v4/rooms/r/occupancy"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(r#"{"connections":3,"presenceMembers":2}"#),
)
.mount(&server)
.await;
let client = Client::builder(Auth::api_key("k:s"))
.host(server.uri())
.build();
let r = client
.inner
.send(Method::GET, "/chat/v4/rooms/r/occupancy", &[], None, false)
.await
.unwrap();
assert_eq!(r.status, 200);
let occ: crate::types::Occupancy = serde_json::from_slice(&r.body).unwrap();
assert_eq!(occ.connections, 3);
assert_eq!(occ.presence_members, 2);
}
}