use std::marker::PhantomData;
use std::sync::Arc;
use bytes::Bytes;
use http::header::{HeaderValue, CONTENT_TYPE};
use http::Method;
use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::client::HttpClient;
use crate::hooks::{Cost, DynAuthenticator, RequestParts};
use crate::response::Response;
use crate::ApiError;
pub trait QueryBuilder: Sized {
fn add_query(&mut self, key: String, value: String);
fn query(mut self, key: impl Into<String>, value: impl ToString) -> Self {
self.add_query(key.into(), value.to_string());
self
}
fn query_opt(mut self, key: impl Into<String>, value: Option<impl ToString>) -> Self {
if let Some(v) = value {
self.add_query(key.into(), v.to_string());
}
self
}
fn query_many<I, V>(self, key: impl Into<String>, values: I) -> Self
where
I: IntoIterator<Item = V>,
V: ToString,
{
let key = key.into();
let mut result = self;
for value in values {
result.add_query(key.clone(), value.to_string());
}
result
}
fn query_many_opt<I, V>(self, key: impl Into<String>, values: Option<I>) -> Self
where
I: IntoIterator<Item = V>,
V: ToString,
{
if let Some(values) = values {
self.query_many(key, values)
} else {
self
}
}
}
pub trait RequestError: From<ApiError> + std::fmt::Debug {}
impl<E: From<ApiError> + std::fmt::Debug> RequestError for E {}
pub struct Request<T, E> {
pub(crate) http_client: HttpClient,
pub(crate) method: Method,
pub(crate) path: String,
pub(crate) query: Vec<(String, String)>,
pub(crate) body: Option<Bytes>,
pub(crate) authenticator: Option<Arc<DynAuthenticator<'static>>>,
pub(crate) costs: Vec<Cost>,
pub(crate) _marker: PhantomData<(T, E)>,
}
impl<T, E> Request<T, E> {
pub fn new(http_client: HttpClient, path: impl Into<String>) -> Self {
Self {
http_client,
method: Method::GET,
path: path.into(),
query: Vec::new(),
body: None,
authenticator: None,
costs: Vec::new(),
_marker: PhantomData,
}
}
pub fn method(mut self, method: Method) -> Self {
self.method = method;
self
}
pub fn authenticator(mut self, auth: Arc<DynAuthenticator<'static>>) -> Self {
self.authenticator = Some(auth);
self
}
pub fn with_cost(mut self, cost: Cost) -> Self {
self.costs.push(cost);
self
}
}
impl<T, E: From<ApiError>> Request<T, E> {
pub fn body<B: Serialize + ?Sized>(mut self, body: &B) -> Result<Self, E> {
let value = serde_json::to_value(body).map_err(ApiError::from)?;
self.body = Some(Bytes::from(value.to_string()));
Ok(self)
}
}
impl<T, E> Clone for Request<T, E> {
fn clone(&self) -> Self {
Self {
http_client: self.http_client.clone(),
method: self.method.clone(),
path: self.path.clone(),
query: self.query.clone(),
body: self.body.clone(),
authenticator: self.authenticator.clone(),
costs: self.costs.clone(),
_marker: PhantomData,
}
}
}
impl<T, E> QueryBuilder for Request<T, E> {
fn add_query(&mut self, key: String, value: String) {
self.query.push((key, value));
}
}
impl<T: DeserializeOwned, E: RequestError> Request<T, E> {
pub async fn send(self) -> Result<T, E> {
let path = self.path.clone();
let response = self.send_raw().await?;
let text = response.text().await.map_err(E::from)?;
crate::decode_json(&path, &text).map_err(|e| E::from(ApiError::from(e)))
}
pub async fn send_raw(self) -> Result<Response, E> {
let mut parts = RequestParts::new(self.method, self.path);
parts.query = self.query;
if let Some(body) = self.body {
parts
.headers
.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
parts.body = Some(body);
}
let failure = match self
.http_client
.send(parts, &self.costs, self.authenticator.as_deref())
.await
{
Ok(response) if response.status().is_success() => {
tracing::debug!("Response status: {}", response.status());
return Ok(response);
}
Ok(response) => ApiError::from_response(response).await,
Err(err) => err,
};
let answered = matches!(failure, ApiError::Response(_));
let error = E::from(failure);
if answered {
tracing::error!("Request failed: {:?}", error);
}
Err(error)
}
}
pub struct TypedRequest<T> {
pub(crate) _marker: PhantomData<T>,
}
impl<T> TypedRequest<T> {
pub fn new() -> Self {
Self {
_marker: PhantomData,
}
}
}
impl<T> Default for TypedRequest<T> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::HttpClientBuilder;
fn make_request() -> Request<(), ApiError> {
let http = HttpClientBuilder::new("https://example.com")
.build()
.unwrap();
Request::new(http, "/test")
}
#[test]
fn test_query_adds_key_value() {
let req = make_request().query("limit", 10);
assert_eq!(req.query, vec![("limit".into(), "10".into())]);
}
#[test]
fn test_query_chaining_preserves_order() {
let req = make_request()
.query("limit", 10)
.query("offset", "abc")
.query("active", true);
assert_eq!(
req.query,
vec![
("limit".into(), "10".into()),
("offset".into(), "abc".into()),
("active".into(), "true".into()),
]
);
}
#[test]
fn test_query_opt_some_adds_parameter() {
let req = make_request().query_opt("tag", Some("politics"));
assert_eq!(req.query, vec![("tag".into(), "politics".into())]);
}
#[test]
fn test_query_opt_none_skips_parameter() {
let req = make_request().query_opt("tag", None::<&str>);
assert!(req.query.is_empty());
}
#[test]
fn test_query_opt_interleaved_with_query() {
let req = make_request()
.query("limit", 25)
.query_opt("cursor", None::<String>)
.query("active", true)
.query_opt("slug", Some("will-x-happen"));
assert_eq!(
req.query,
vec![
("limit".into(), "25".into()),
("active".into(), "true".into()),
("slug".into(), "will-x-happen".into()),
]
);
}
#[test]
fn test_query_many_adds_repeated_key() {
let req = make_request().query_many("id", vec!["abc", "def", "ghi"]);
assert_eq!(
req.query,
vec![
("id".into(), "abc".into()),
("id".into(), "def".into()),
("id".into(), "ghi".into()),
]
);
}
#[test]
fn test_query_many_empty_iterator() {
let req = make_request().query_many("id", Vec::<String>::new());
assert!(req.query.is_empty());
}
#[test]
fn test_query_many_opt_some_adds_values() {
let ids = vec![1u64, 2, 3];
let req = make_request().query_many_opt("id", Some(ids));
assert_eq!(
req.query,
vec![
("id".into(), "1".into()),
("id".into(), "2".into()),
("id".into(), "3".into()),
]
);
}
#[test]
fn test_query_many_opt_none_skips() {
let req = make_request().query_many_opt("id", None::<Vec<String>>);
assert!(req.query.is_empty());
}
#[test]
fn test_query_duplicate_keys_allowed() {
let req = make_request()
.query("sort", "price")
.query("sort", "volume");
assert_eq!(
req.query,
vec![
("sort".into(), "price".into()),
("sort".into(), "volume".into()),
]
);
}
#[test]
fn test_request_new_stores_path() {
let req = make_request();
assert_eq!(req.path, "/test");
assert!(req.query.is_empty());
}
#[test]
fn test_request_new_with_string_path() {
let http = HttpClientBuilder::new("https://example.com")
.build()
.unwrap();
let req: Request<(), ApiError> = Request::new(http, String::from("/events"));
assert_eq!(req.path, "/events");
}
#[test]
fn test_request_clone_does_not_require_clone_markers() {
struct NotClone;
struct NotCloneError;
let http = HttpClientBuilder::new("https://example.com")
.build()
.unwrap();
let original: Request<NotClone, NotCloneError> =
Request::new(http, "/feed").query("user", "0xabc");
let extended = original.clone().query("cursor", "c1");
assert_eq!(original.path, "/feed");
assert_eq!(original.query, vec![("user".into(), "0xabc".into())]);
assert_eq!(extended.path, "/feed");
assert_eq!(
extended.query,
vec![
("user".into(), "0xabc".into()),
("cursor".into(), "c1".into()),
]
);
}
#[test]
fn test_typed_request_new_and_default() {
let _t1: TypedRequest<String> = TypedRequest::new();
let _t2: TypedRequest<String> = TypedRequest::default();
}
}